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
|
@@ -14,7 +14,8 @@ module RubyLLM
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def api_base
|
|
17
|
-
|
|
17
|
+
configured_api_base(:elevenlabs_api_base, 'https://api.elevenlabs.io',
|
|
18
|
+
endpoints: %r{/v1/(?:speech-to-text\z|text-to-speech/.*\z)})
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def headers
|
|
@@ -4,12 +4,15 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Native Gemini API implementation
|
|
6
6
|
class Gemini < Provider
|
|
7
|
+
include Protocols::Gemini::RetryInfo
|
|
8
|
+
|
|
7
9
|
protocol :gemini, Protocols::Gemini, batches: Protocols::Gemini::Batches
|
|
8
10
|
protocol :interactions, Protocols::Interactions
|
|
9
11
|
protocol :live_transcription, Protocols::Gemini::LiveTranscription
|
|
10
12
|
protocol :files, Protocols::Gemini::Files
|
|
11
13
|
|
|
12
14
|
def protocol_for(model, operation: nil, **)
|
|
15
|
+
return protocols[:interactions] if operation == :animate
|
|
13
16
|
return protocols[:interactions] if operation == :transcribe && model.id == 'gemini-3.5-transcribe'
|
|
14
17
|
return protocols[:live_transcription] if operation == :transcribe && model.id == 'gemini-3.5-transcribe-live'
|
|
15
18
|
|
|
@@ -17,7 +20,8 @@ module RubyLLM
|
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def api_base
|
|
20
|
-
|
|
23
|
+
configured_api_base(:gemini_api_base, 'https://generativelanguage.googleapis.com/v1beta',
|
|
24
|
+
endpoints: %r{/models/[^/]+:\w+\z})
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def headers
|
|
@@ -35,6 +39,14 @@ module RubyLLM
|
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
class << self
|
|
42
|
+
def open_telemetry_name # :nodoc:
|
|
43
|
+
'gcp.gemini'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def models_dev_provider_ids # :nodoc:
|
|
47
|
+
%w[google]
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def capabilities
|
|
39
51
|
Gemini::Capabilities
|
|
40
52
|
end
|
|
@@ -7,13 +7,33 @@ module RubyLLM
|
|
|
7
7
|
module Chat
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
GPUStack::Media.
|
|
10
|
+
def render_content(content, attachments = [])
|
|
11
|
+
GPUStack::Media.render_content(content, attachments)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def render_role(role)
|
|
15
15
|
role.to_s
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# vLLM truncates the rendered prompt to its last tokens. It summarizes
|
|
19
|
+
# nothing, and the cut can reach the system prompt. Truncating to the
|
|
20
|
+
# model's own limit without an output limit leaves no room to answer,
|
|
21
|
+
# and vLLM rejects the request.
|
|
22
|
+
def apply_compaction(payload, compaction)
|
|
23
|
+
log_ignored_compaction_options(compaction)
|
|
24
|
+
limit = compaction[:at] || (-1 if payload.key?(:max_tokens))
|
|
25
|
+
return payload.merge(truncate_prompt_tokens: limit, truncation_side: 'left') if limit
|
|
26
|
+
|
|
27
|
+
RubyLLM.logger.debug { "#{@provider.name} needs at: or an output limit to truncate, not truncating" }
|
|
28
|
+
payload
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def log_ignored_compaction_options(compaction)
|
|
32
|
+
ignored = compaction.except(:at)
|
|
33
|
+
return if ignored.empty?
|
|
34
|
+
|
|
35
|
+
RubyLLM.logger.debug { "#{@provider.name} only truncates the prompt, dropping #{ignored.inspect}" }
|
|
36
|
+
end
|
|
17
37
|
end
|
|
18
38
|
end
|
|
19
39
|
end
|
|
@@ -7,7 +7,7 @@ module RubyLLM
|
|
|
7
7
|
module Embeddings
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def embedding_media?
|
|
11
11
|
true
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ module RubyLLM
|
|
|
18
18
|
raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
|
|
19
19
|
|
|
20
20
|
payload.delete(:input)
|
|
21
|
-
payload[:messages] = [{ role: 'user', content: GPUStack::Media.
|
|
21
|
+
payload[:messages] = [{ role: 'user', content: GPUStack::Media.render_content(text, with) }]
|
|
22
22
|
end
|
|
23
23
|
payload.merge(provider_options)
|
|
24
24
|
end
|
|
@@ -11,24 +11,24 @@ module RubyLLM
|
|
|
11
11
|
%i[image audio video 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
|
-
|
|
20
|
+
render_image(attachment)
|
|
21
21
|
when :audio
|
|
22
|
-
Protocols::ChatCompletions::Media.
|
|
22
|
+
Protocols::ChatCompletions::Media.render_audio(attachment)
|
|
23
23
|
when :video
|
|
24
|
-
|
|
24
|
+
render_video(attachment)
|
|
25
25
|
when :text
|
|
26
|
-
Protocols::ChatCompletions::Media.
|
|
26
|
+
Protocols::ChatCompletions::Media.render_text_file(attachment)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def render_image(image)
|
|
32
32
|
{
|
|
33
33
|
type: 'image_url',
|
|
34
34
|
image_url: {
|
|
@@ -38,7 +38,7 @@ module RubyLLM
|
|
|
38
38
|
}
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
41
|
+
def render_video(video)
|
|
42
42
|
{
|
|
43
43
|
type: 'video_url',
|
|
44
44
|
video_url: { url: video.for_llm }
|
|
@@ -7,19 +7,27 @@ module RubyLLM
|
|
|
7
7
|
module Transcription
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# Usage may follow the finish reason in an event of its own, so only
|
|
11
|
+
# the end of the stream can say the transcript is done.
|
|
12
|
+
def stream_transcription(payload, model:, &block)
|
|
13
|
+
last = nil
|
|
14
|
+
result = super({ stream_include_usage: 'true' }.merge(payload), model:) do |chunk|
|
|
15
|
+
last = chunk
|
|
16
|
+
block.call(chunk) unless chunk.done?
|
|
17
|
+
end
|
|
18
|
+
block.call(RubyLLM::TranscriptionChunk.new(type: RubyLLM::TranscriptionChunk::DONE, text: result.text,
|
|
19
|
+
raw: last&.raw))
|
|
20
|
+
result
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
def build_transcription_chunk(data)
|
|
15
24
|
return super unless data.key?('choices')
|
|
16
25
|
|
|
17
|
-
choice = data['choices'].first
|
|
18
|
-
finished = choice['finish_reason'] || data['choices'].empty?
|
|
26
|
+
choice = data['choices'].first
|
|
19
27
|
|
|
20
28
|
RubyLLM::TranscriptionChunk.new(
|
|
21
|
-
type:
|
|
22
|
-
delta: choice
|
|
29
|
+
type: choice ? RubyLLM::TranscriptionChunk::DELTA : RubyLLM::TranscriptionChunk::DONE,
|
|
30
|
+
delta: choice&.dig('delta', 'content'),
|
|
23
31
|
raw: data
|
|
24
32
|
)
|
|
25
33
|
end
|
|
@@ -7,22 +7,39 @@ module RubyLLM
|
|
|
7
7
|
module Chat
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
REJECTED_EFFORT = /\AUnexpected reasoning effort /
|
|
11
|
+
|
|
12
|
+
def render_role(role)
|
|
11
13
|
role.to_s
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
# The API accepts cache fields and ignores them; it caches long
|
|
17
|
+
# prompts by itself.
|
|
18
|
+
def openai_prompt_caching?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply_prompt_cache_params(_payload, caching)
|
|
23
|
+
prompt_cache_options(caching) if caching
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The API names a rejected effort only in its message.
|
|
27
|
+
def parse_unsupported_parameter(response)
|
|
28
|
+
super || (:thinking if parse_error(response).to_s.match?(REJECTED_EFFORT))
|
|
29
|
+
end
|
|
30
|
+
|
|
14
31
|
def supported_attachment?(attachment)
|
|
15
32
|
Protocols::ChatCompletions::Media.supported_attachment?(
|
|
16
33
|
attachment, document_attachments: :none, audio_attachments: false
|
|
17
34
|
)
|
|
18
35
|
end
|
|
19
36
|
|
|
20
|
-
def
|
|
21
|
-
Protocols::ChatCompletions::Media.
|
|
37
|
+
def render_content(content, attachments = [])
|
|
38
|
+
Protocols::ChatCompletions::Media.render_parts(content, attachments) do |attachment|
|
|
22
39
|
if attachment.type == :image && !attachment.provider_file?
|
|
23
|
-
|
|
40
|
+
render_inline_image(attachment)
|
|
24
41
|
else
|
|
25
|
-
Protocols::ChatCompletions::Media.
|
|
42
|
+
Protocols::ChatCompletions::Media.render_attachment(
|
|
26
43
|
attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
|
|
27
44
|
)
|
|
28
45
|
end
|
|
@@ -30,7 +47,7 @@ module RubyLLM
|
|
|
30
47
|
end
|
|
31
48
|
|
|
32
49
|
# Hetzner's servers can't fetch URLs, so remote images travel inline too.
|
|
33
|
-
def
|
|
50
|
+
def render_inline_image(image)
|
|
34
51
|
{ type: 'image_url', image_url: { url: image.for_llm } }
|
|
35
52
|
end
|
|
36
53
|
end
|
|
@@ -12,7 +12,7 @@ module RubyLLM
|
|
|
12
12
|
protocol :chat_completions, ChatCompletions
|
|
13
13
|
|
|
14
14
|
def api_base
|
|
15
|
-
|
|
15
|
+
configured_api_base(:hetzner_api_base, 'https://inference.hetzner.com/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def headers
|
|
@@ -20,6 +20,10 @@ module RubyLLM
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
class << self
|
|
23
|
+
def models_dev_provider_ids # :nodoc:
|
|
24
|
+
%w[hetzner]
|
|
25
|
+
end
|
|
26
|
+
|
|
23
27
|
def configuration_options
|
|
24
28
|
%i[hetzner_api_key hetzner_api_base]
|
|
25
29
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Meta
|
|
6
|
+
# Meta's dialect of the OpenAI Responses API, the format Meta
|
|
7
|
+
# recommends because it replays encrypted reasoning across turns.
|
|
8
|
+
# https://dev.meta.ai/docs/protocols/responses
|
|
9
|
+
class Responses < Protocols::Responses
|
|
10
|
+
include Protocols::Responses::TokenCounting
|
|
11
|
+
include Protocols::Meta::Transcription
|
|
12
|
+
|
|
13
|
+
SERVER_TOOL_ALIASES = {
|
|
14
|
+
web_search: { tool: { type: 'web_search' } }
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
PROMPT_CACHE_OPTIONS = %i[key ttl].freeze
|
|
18
|
+
AUDIO_FORMATS = %w[mp3 wav].freeze
|
|
19
|
+
|
|
20
|
+
def server_tool_aliases
|
|
21
|
+
SERVER_TOOL_ALIASES
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def document_urls?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def web_search_sources?
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def apply_end_user(payload, identifier)
|
|
33
|
+
payload.merge(safety_identifier: identifier)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Meta takes a cache key and a retention hint, in_memory or 24h,
|
|
37
|
+
# and no cache breakpoints.
|
|
38
|
+
def prompt_cache_params(caching)
|
|
39
|
+
options = caching.to_h.transform_keys(&:to_sym)
|
|
40
|
+
unsupported = options.keys - PROMPT_CACHE_OPTIONS
|
|
41
|
+
unless unsupported.empty?
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"Meta prompt caching accepts :key and :ttl, got #{format_cache_option_keys(unsupported)}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
ttl = options[:ttl] && render_cache_lifetime(options[:ttl])
|
|
47
|
+
{ prompt_cache_key: options[:key], prompt_cache_retention: ttl }.compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cache_option_names = PROMPT_CACHE_OPTIONS
|
|
51
|
+
|
|
52
|
+
def inject_cache_breakpoint(item)
|
|
53
|
+
item
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def supported_attachment?(attachment)
|
|
57
|
+
return super if attachment.provider_file?
|
|
58
|
+
return AUDIO_FORMATS.include?(attachment.format) if attachment.audio?
|
|
59
|
+
|
|
60
|
+
attachment.video? || super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Meta's fetcher is refused by many hosts, so remote media travels inline.
|
|
64
|
+
def render_attachment(attachment)
|
|
65
|
+
return super if attachment.provider_file? || !supported_attachment?(attachment)
|
|
66
|
+
|
|
67
|
+
case attachment.type
|
|
68
|
+
when :video then { type: 'input_video', video_url: attachment.for_llm }
|
|
69
|
+
when :audio then { type: 'input_audio', input_audio: { data: attachment.encoded, format: attachment.format } }
|
|
70
|
+
else super
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Meta refuses to fetch many remote images, so images travel inline.
|
|
75
|
+
def render_image(image, image_url: image.for_llm, **)
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Meta Model API integration for the Muse models.
|
|
6
|
+
class Meta < Provider
|
|
7
|
+
protocol :responses, Meta::Responses
|
|
8
|
+
protocol :files, Protocols::OpenAI::Files
|
|
9
|
+
|
|
10
|
+
def api_base
|
|
11
|
+
configured_api_base(:meta_api_base, 'https://api.meta.ai/v1', endpoints: Protocols::ChatCompletions::ENDPOINT_PATHS)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def headers
|
|
15
|
+
{ 'Authorization' => "Bearer #{@config.meta_api_key}" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def account_identity
|
|
19
|
+
account_digest(api_base, @config.meta_api_key)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def models_dev_provider_ids # :nodoc:
|
|
24
|
+
%w[meta]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configuration_options
|
|
28
|
+
%i[meta_api_key meta_api_base]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configuration_requirements
|
|
32
|
+
%i[meta_api_key]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class MiniMax
|
|
6
|
+
# MiniMax's dialect of the Anthropic Messages API. MiniMax has no
|
|
7
|
+
# Files API on this endpoint, and web search is the one server tool it
|
|
8
|
+
# runs, in Anthropic's first version.
|
|
9
|
+
class Anthropic < Protocols::Anthropic
|
|
10
|
+
SERVER_TOOL_ALIASES = {
|
|
11
|
+
web_search: { tool: { type: 'web_search_20250305', name: 'web_search' } }
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
RESPONSES_ITEMS = %w[reasoning web_search_call message function_call].freeze
|
|
15
|
+
private_constant :RESPONSES_ITEMS
|
|
16
|
+
|
|
17
|
+
def render_payload(messages, tools:, schema: nil, tool_prefs: nil, **)
|
|
18
|
+
raise ArgumentError, "MiniMax's Messages API has no structured output" if schema
|
|
19
|
+
raise ArgumentError, 'MiniMax does not accept calls: :one' if tools.any? && tool_prefs&.dig(:calls) == :one
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render_count_tokens_payload(messages, tool_prefs: nil, **)
|
|
25
|
+
super(messages, **, schema: nil, tool_prefs: tool_prefs&.except(:calls))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def server_tool_aliases
|
|
29
|
+
SERVER_TOOL_ALIASES
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def provider_file_references?
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supported_attachment?(attachment)
|
|
37
|
+
return attachment.source.provider == 'minimax' if attachment.provider_file? && attachment.video?
|
|
38
|
+
|
|
39
|
+
attachment.video? || super
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# A video block is documented for user messages only; tool results
|
|
45
|
+
# and system prompts take the attachments Anthropic's format does.
|
|
46
|
+
def supported_message_attachment?(message, attachment)
|
|
47
|
+
return false if attachment.video? && message.role != :user
|
|
48
|
+
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_content(content, attachments = [], citations: false)
|
|
53
|
+
blocks = super(content, [], citations:)
|
|
54
|
+
attachments.each do |attachment|
|
|
55
|
+
blocks.concat(attachment.video? ? [render_video(attachment)] : super(nil, [attachment], citations:))
|
|
56
|
+
end
|
|
57
|
+
blocks
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def append_formatted_content(content_blocks, msg, citations: false)
|
|
61
|
+
video = msg.attachments.find(&:video?) unless msg.role == :user
|
|
62
|
+
raise UnsupportedAttachmentError, video.mime_type if video
|
|
63
|
+
|
|
64
|
+
content_blocks.concat(render_content(msg.content, msg.attachments, citations:))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_video(video)
|
|
68
|
+
raise UnsupportedAttachmentError, video.mime_type unless supported_attachment?(video)
|
|
69
|
+
|
|
70
|
+
{ type: 'video', source: video_source(video) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# MiniMax's fetcher is refused by many hosts, so remote videos travel inline.
|
|
74
|
+
def video_source(video)
|
|
75
|
+
return { type: 'url', url: "mm_file://#{video.provider_file_id}" } if video.provider_file?
|
|
76
|
+
|
|
77
|
+
media_type = video.mime_type == 'video/quicktime' ? 'video/mov' : video.mime_type
|
|
78
|
+
{ type: 'base64', media_type:, data: video.encoded }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A web search turn answered on Responses keeps that route's output
|
|
82
|
+
# items, which a Messages request cannot carry.
|
|
83
|
+
def foreign_native_content?(message)
|
|
84
|
+
super || (message.role == :assistant && responses_output?(message.raw_content))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def responses_output?(raw_content)
|
|
88
|
+
raw_content.is_a?(Array) && raw_content.any? do |item|
|
|
89
|
+
item.is_a?(Hash) && RESPONSES_ITEMS.include?(item['type'])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class MiniMax
|
|
6
|
+
# MiniMax's own Chat Completion v2 route, used only for structured
|
|
7
|
+
# output on the one model that applies response_format. That model
|
|
8
|
+
# nests its answer under the schema name, so parsing removes the
|
|
9
|
+
# wrapper, which cannot be done part by part on a stream.
|
|
10
|
+
class ChatCompletions < Protocols::ChatCompletions
|
|
11
|
+
def completion_url
|
|
12
|
+
"#{@provider.media_base}/v1/text/chatcompletion_v2"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_payload(messages, schema: nil, stream: false, **)
|
|
16
|
+
raise ArgumentError, "#{@model.id} does not stream structured output" if schema && stream
|
|
17
|
+
|
|
18
|
+
@schema_name = schema&.dig(:name)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_completion_body(data, raw:)
|
|
23
|
+
super(unwrap_schema_result(data), raw: raw)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def unwrap_schema_result(data)
|
|
29
|
+
content = data.dig('choices', 0, 'message', 'content')
|
|
30
|
+
return data unless @schema_name && content.is_a?(String)
|
|
31
|
+
|
|
32
|
+
parsed = parse_json(content)
|
|
33
|
+
return data unless parsed.is_a?(Hash) && parsed.keys == [@schema_name]
|
|
34
|
+
|
|
35
|
+
choice = data['choices'][0]
|
|
36
|
+
message = choice['message'].merge('content' => JSON.generate(parsed[@schema_name]))
|
|
37
|
+
data.merge('choices' => [choice.merge('message' => message), *data['choices'][1..]])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_json(content)
|
|
41
|
+
JSON.parse(content)
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class MiniMax
|
|
6
|
+
# MiniMax's Responses route, used for web search because only this
|
|
7
|
+
# endpoint returns citations. It holds no state: store is always false.
|
|
8
|
+
# https://platform.minimax.io/docs/api-reference/responses-create
|
|
9
|
+
class Responses < Protocols::Responses
|
|
10
|
+
SERVER_TOOL_ALIASES = {
|
|
11
|
+
web_search: { tool: { type: 'web_search' } }
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# MiniMax reports a search's pages only through its citations.
|
|
15
|
+
def self.parse_web_sources(message)
|
|
16
|
+
message.citations.filter_map do |citation|
|
|
17
|
+
WebSource.new(url: citation.url, title: citation.title) if citation.url
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def completion_url
|
|
22
|
+
"#{@provider.media_base}/v1/responses"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def server_tool_aliases
|
|
26
|
+
SERVER_TOOL_ALIASES
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def provider_file_references?
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# MiniMax sends url_citation annotations with both offsets at 0, so
|
|
36
|
+
# they say nothing about the span they support. Its content field is
|
|
37
|
+
# the quoted snippet.
|
|
38
|
+
def parse_annotations(annotations, _content)
|
|
39
|
+
Array(annotations).filter_map do |annotation|
|
|
40
|
+
next unless annotation['type'] == 'url_citation'
|
|
41
|
+
|
|
42
|
+
Citation.new(url: annotation['url'], title: annotation['title'], cited_text: annotation['content'])
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def offset_citations(citations, *)
|
|
47
|
+
citations
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|