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
|
@@ -27,12 +27,18 @@ module RubyLLM
|
|
|
27
27
|
|
|
28
28
|
def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
|
|
29
29
|
schema: nil, thinking: nil, tool_prefs: nil, **)
|
|
30
|
+
raise ArgumentError, 'Mistral has no thinking budget; use effort:' if thinking&.budget
|
|
31
|
+
|
|
30
32
|
options = super(messages, tools:, temperature:, model:, stream:, max_output_tokens:,
|
|
31
33
|
schema:, thinking:, citations: false, caching: nil, tool_prefs:)
|
|
32
34
|
normalize_conversation_choice(options)
|
|
35
|
+
if options[:parallel_tool_calls] == false
|
|
36
|
+
raise ArgumentError, 'Mistral Conversations does not accept calls: :one'
|
|
37
|
+
end
|
|
38
|
+
|
|
33
39
|
{
|
|
34
40
|
model: model.id,
|
|
35
|
-
inputs:
|
|
41
|
+
inputs: render_entries(messages),
|
|
36
42
|
instructions: messages.select { |message| message.role == :system }.map(&:content).join("\n\n"),
|
|
37
43
|
completion_args: options.slice(:temperature, :max_tokens, :response_format, :reasoning_effort,
|
|
38
44
|
:tool_choice),
|
|
@@ -51,19 +57,39 @@ module RubyLLM
|
|
|
51
57
|
options[:tool_choice] = 'any' if choice == 'required'
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
def
|
|
55
|
-
entries = messages.reject { |message| message.role == :system }
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
elsif message.tool_result?
|
|
59
|
-
[{ type: 'function.result', tool_call_id: message.tool_call_id, result: message.content.to_s }]
|
|
60
|
-
else
|
|
61
|
-
format_conversation_message(message)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
60
|
+
def render_entries(messages)
|
|
61
|
+
entries = messages.reject { |message| message.role == :system }
|
|
62
|
+
.chunk_while { |previous, current| previous.tool_result? && current.tool_result? }
|
|
63
|
+
.flat_map { |group| render_entry_group(group) }
|
|
64
64
|
entries.each_with_index.flat_map { |entry, index| replay_conversation_entry(entry, index) }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Function results are text-only on the wire and a round's results
|
|
68
|
+
# stay consecutive, so tool attachments ride a user entry after them.
|
|
69
|
+
def render_entry_group(group)
|
|
70
|
+
carriers = group.select do |message|
|
|
71
|
+
message.tool_result? && message.raw_content.nil? && message.attachments.any?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
group.flat_map { |message| render_entry(message) } +
|
|
75
|
+
carriers.map { |message| { type: 'message.input', **tool_attachment_message(message) } }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def render_entry(message)
|
|
79
|
+
if conversation_entries?(message.raw_content)
|
|
80
|
+
Protocol::ReplayedArguments.blank(message.raw_content, message,
|
|
81
|
+
id: 'tool_call_id', arguments: 'arguments', empty: '{}')
|
|
82
|
+
elsif message.tool_result?
|
|
83
|
+
[{ type: 'function.result', tool_call_id: message.provider_call_id, result: message.content.to_s }]
|
|
84
|
+
else
|
|
85
|
+
render_conversation_message(message)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def conversation_entries?(content)
|
|
90
|
+
content.is_a?(Array) && content.all? { |entry| entry.is_a?(Hash) && entry['type'] }
|
|
91
|
+
end
|
|
92
|
+
|
|
67
93
|
def replay_conversation_entry(entry, index)
|
|
68
94
|
return replay_conversation_execution(entry, index) if entry['type'] == 'tool.execution'
|
|
69
95
|
|
|
@@ -91,16 +117,16 @@ module RubyLLM
|
|
|
91
117
|
]
|
|
92
118
|
end
|
|
93
119
|
|
|
94
|
-
def
|
|
120
|
+
def render_conversation_message(message)
|
|
95
121
|
entries = []
|
|
96
122
|
if message.content || message.attachments.any?
|
|
97
123
|
entries << {
|
|
98
124
|
type: 'message.input', role: message.role.to_s,
|
|
99
|
-
content:
|
|
125
|
+
content: render_message_content(message)
|
|
100
126
|
}
|
|
101
127
|
end
|
|
102
|
-
message.tool_calls
|
|
103
|
-
entries << { type: 'function.call', tool_call_id: call.
|
|
128
|
+
message.tool_calls.each do |call|
|
|
129
|
+
entries << { type: 'function.call', tool_call_id: call.provider_call_id, name: call.name,
|
|
104
130
|
arguments: JSON.generate(call.arguments) }
|
|
105
131
|
end
|
|
106
132
|
entries
|
|
@@ -127,13 +153,10 @@ module RubyLLM
|
|
|
127
153
|
response: raw)
|
|
128
154
|
end
|
|
129
155
|
|
|
130
|
-
output.select { |entry| pending_conversation_call?(entry) }.
|
|
131
|
-
|
|
132
|
-
{ 'id' => entry['tool_call_id'], 'type' => 'function',
|
|
133
|
-
'function' => entry.slice('name', 'arguments') }
|
|
134
|
-
], response: raw, finish_reason: :tool_calls).values.first
|
|
135
|
-
[call.id, call]
|
|
156
|
+
calls = output.select { |entry| pending_conversation_call?(entry) }.map do |entry|
|
|
157
|
+
{ 'id' => entry['tool_call_id'], 'type' => 'function', 'function' => entry.slice('name', 'arguments') }
|
|
136
158
|
end
|
|
159
|
+
parse_tool_calls(calls, response: raw, finish_reason: :tool_calls) || {}
|
|
137
160
|
end
|
|
138
161
|
|
|
139
162
|
def pending_conversation_call?(entry)
|
|
@@ -5,12 +5,14 @@ module RubyLLM
|
|
|
5
5
|
module Mistral
|
|
6
6
|
# Mistral Files API.
|
|
7
7
|
class Files < Protocols::Files
|
|
8
|
+
def rejected_options(operation)
|
|
9
|
+
operation == :upload ? %i[uri] : super
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
private
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
display_name: nil, uri: nil, content_type: nil)
|
|
13
|
-
multipart_payload(attachment, purpose:, expiry: expiry_hours(expires_in), visibility:)
|
|
14
|
+
def render_upload_fields(_attachment, purpose: nil, expires_in: nil)
|
|
15
|
+
{ purpose:, expiry: expiry_hours(expires_in) }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def expiry_hours(expires_in)
|
|
@@ -17,12 +17,23 @@ module RubyLLM
|
|
|
17
17
|
SERVER_TOOL_ALIASES
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def render_message_group(group, **options)
|
|
21
21
|
content = group.first.raw_content
|
|
22
22
|
return super unless group.one? && content.is_a?(Array) &&
|
|
23
|
-
content.all? { |entry| entry.is_a?(Hash) && entry['role'] }
|
|
23
|
+
content.all? { |entry| entry.is_a?(Hash) && entry['role'] && !entry['type'] }
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
ids = Protocol::ReplayedArguments.malformed_call_ids(group.first)
|
|
26
|
+
content.map { |entry| replayed_multi_entry(entry.except('index'), ids) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def replayed_multi_entry(entry, malformed_ids)
|
|
30
|
+
return entry if malformed_ids.empty? || !entry['tool_calls'].is_a?(Array)
|
|
31
|
+
|
|
32
|
+
entry.merge('tool_calls' => entry['tool_calls'].map do |call|
|
|
33
|
+
next call unless malformed_ids.include?(call['id'])
|
|
34
|
+
|
|
35
|
+
call.merge('function' => call['function'].to_h.merge('arguments' => '{}'))
|
|
36
|
+
end)
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
def parse_completion_body(data, raw:)
|
|
@@ -116,7 +127,7 @@ module RubyLLM
|
|
|
116
127
|
append_multi_content(entry, delta['content'])
|
|
117
128
|
append_multi_calls(entry, delta['tool_calls'])
|
|
118
129
|
@multi_finish_reason = choice['finish_reason'] if choice['finish_reason']
|
|
119
|
-
content, thinking =
|
|
130
|
+
content, thinking = parse_content_and_thinking(delta['content']) if entry['role'] == 'assistant'
|
|
120
131
|
Chunk.new(role: :assistant, content: content, thinking: Thinking.build(text: thinking), model: data['model'])
|
|
121
132
|
end
|
|
122
133
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
module Moonshot
|
|
6
|
+
# Kimi's Files API. A file's purpose decides what Kimi does with it, so
|
|
7
|
+
# an upload without one follows its content: images and videos are
|
|
8
|
+
# stored for the models to read, and everything else has its text
|
|
9
|
+
# extracted.
|
|
10
|
+
class Files < Protocols::OpenAI::Files
|
|
11
|
+
def rejected_options(operation)
|
|
12
|
+
operation == :upload ? %i[expires_in uri] : super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Kimi wraps a file's extracted text in a JSON object; any other
|
|
16
|
+
# stored file comes back as it was written.
|
|
17
|
+
def download(file_id)
|
|
18
|
+
body = super
|
|
19
|
+
extracted_text(body) || body
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def render_upload_fields(attachment, purpose: nil, **)
|
|
25
|
+
super(attachment, purpose: purpose || default_purpose(attachment))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def default_purpose(attachment)
|
|
29
|
+
return 'image' if attachment.image?
|
|
30
|
+
return 'video' if attachment.video?
|
|
31
|
+
|
|
32
|
+
'file-extract'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_file_response(data, mime_type: nil)
|
|
36
|
+
super(data, mime_type: mime_type || data['file_type'])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extracted_text(body)
|
|
40
|
+
data = JSON.parse(body)
|
|
41
|
+
data['content'] if data.is_a?(Hash) && data['type'] == 'file'
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -114,8 +114,9 @@ module RubyLLM
|
|
|
114
114
|
index = batch_result_index(line['custom_id'])
|
|
115
115
|
response = line['response']
|
|
116
116
|
|
|
117
|
-
if
|
|
118
|
-
|
|
117
|
+
if batch_response_succeeded?(response)
|
|
118
|
+
body = response['body']
|
|
119
|
+
parsed_batch_result(index, line['custom_id'], body) { parse_batch_completion_response(body) }
|
|
119
120
|
else
|
|
120
121
|
[index, nil, batch_failure(line['custom_id'], batch_error_message(line))]
|
|
121
122
|
end
|
|
@@ -27,13 +27,15 @@ module RubyLLM
|
|
|
27
27
|
|
|
28
28
|
def parse_judgment_response(response, questions:)
|
|
29
29
|
body = response.body
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
named = body.fetch('answers').to_h { |answer| [answer.fetch('name'), answer] }
|
|
31
|
+
answers = questions.values.to_h do |question|
|
|
32
|
+
[question.name, parse_answer(named.fetch(question.name.to_s), question)]
|
|
32
33
|
end
|
|
33
34
|
RubyLLM::Judgment.new(answers:, model: body.fetch('model'), raw: response, model_info: @model,
|
|
34
35
|
tokens: parse_tokens(body['usage']))
|
|
35
36
|
rescue KeyError, NoMethodError, TypeError => e
|
|
36
|
-
raise Error.new(
|
|
37
|
+
raise Error.new('OpenAI Decisions returned an invalid judgment', provider_message: e.message, response:,
|
|
38
|
+
redact: @config&.redact_error_messages)
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
@@ -51,7 +53,7 @@ module RubyLLM
|
|
|
51
53
|
raise UnsupportedAttachmentError, image.mime_type unless image.image?
|
|
52
54
|
return { type: 'input_image', file_id: image.provider_file_id } if image.provider_file?
|
|
53
55
|
|
|
54
|
-
Responses::Media.
|
|
56
|
+
Responses::Media.render_image(image, image_url: image.for_llm,
|
|
55
57
|
original_detail: @provider.original_image_detail?)
|
|
56
58
|
end
|
|
57
59
|
|
|
@@ -5,19 +5,25 @@ module RubyLLM
|
|
|
5
5
|
module OpenAI
|
|
6
6
|
# OpenAI Files API.
|
|
7
7
|
class Files < Protocols::Files
|
|
8
|
+
def rejected_options(operation)
|
|
9
|
+
operation == :upload ? %i[uri] : super
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
private
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
display_name: nil, uri: nil, content_type: nil)
|
|
13
|
-
multipart_payload(attachment, purpose:, expires_after: expires_after(expires_in))
|
|
14
|
+
def render_upload_fields(_attachment, purpose: nil, expires_in: nil)
|
|
15
|
+
{ purpose:, expires_after: expires_after(expires_in) }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def expires_after(expires_in)
|
|
17
19
|
{ anchor: 'created_at', seconds: expires_in } if expires_in
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
def
|
|
22
|
+
def parse_upload_response(data, attachment)
|
|
23
|
+
parse_file_response(data, mime_type: file_content_type(attachment))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_file_response(data, mime_type: nil)
|
|
21
27
|
uploaded_file(
|
|
22
28
|
data,
|
|
23
29
|
id: data['id'],
|
|
@@ -26,7 +32,8 @@ module RubyLLM
|
|
|
26
32
|
created_at: timestamp(data['created_at']),
|
|
27
33
|
expires_at: timestamp(data['expires_at']),
|
|
28
34
|
status: data['status'],
|
|
29
|
-
purpose: data['purpose']
|
|
35
|
+
purpose: data['purpose'],
|
|
36
|
+
mime_type:
|
|
30
37
|
)
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -7,10 +7,8 @@ module RubyLLM
|
|
|
7
7
|
include RubyLLM::Batch::Helpers
|
|
8
8
|
|
|
9
9
|
TERMINAL_STATUSES = %w[completed failed expired cancelled].freeze
|
|
10
|
-
MEDIA_TYPES = %w[image image_url input_image input_audio audio video video_url input_video file input_file
|
|
11
|
-
document].freeze
|
|
12
10
|
Response = Struct.new(:body)
|
|
13
|
-
private_constant :TERMINAL_STATUSES, :
|
|
11
|
+
private_constant :TERMINAL_STATUSES, :Response
|
|
14
12
|
|
|
15
13
|
def create_batch(requests)
|
|
16
14
|
model = single_batch_model!(requests, 'OpenRouter')
|
|
@@ -68,8 +66,8 @@ module RubyLLM
|
|
|
68
66
|
raise ArgumentError,
|
|
69
67
|
'OpenRouter embedding batches accept text only, without task_type or provider preferences'
|
|
70
68
|
end
|
|
71
|
-
elsif
|
|
72
|
-
raise ArgumentError, 'OpenRouter batches
|
|
69
|
+
elsif body.keys.intersect?(%i[modalities audio image_config])
|
|
70
|
+
raise ArgumentError, 'OpenRouter batches return text only'
|
|
73
71
|
end
|
|
74
72
|
end
|
|
75
73
|
|
|
@@ -81,14 +79,6 @@ module RubyLLM
|
|
|
81
79
|
end
|
|
82
80
|
end
|
|
83
81
|
|
|
84
|
-
def batch_media?(value)
|
|
85
|
-
case value
|
|
86
|
-
when Hash then MEDIA_TYPES.include?(value[:type]) || value.values.any? { |part| batch_media?(part) }
|
|
87
|
-
when Array then value.any? { |part| batch_media?(part) }
|
|
88
|
-
else false
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
82
|
def parse_batch_response(data)
|
|
93
83
|
{
|
|
94
84
|
id: data.fetch('id'), raw_status: data.fetch('status'),
|
|
@@ -116,7 +106,7 @@ module RubyLLM
|
|
|
116
106
|
custom_id, shape = row.fetch('custom_id').split(':', 2)
|
|
117
107
|
index = batch_result_index(custom_id)
|
|
118
108
|
response = row['response']
|
|
119
|
-
unless
|
|
109
|
+
unless batch_response_succeeded?(response) && !row['error']
|
|
120
110
|
return [index, nil, batch_failure(custom_id, batch_error_message(row))]
|
|
121
111
|
end
|
|
122
112
|
|
|
@@ -126,7 +116,10 @@ module RubyLLM
|
|
|
126
116
|
return [index, nil, batch_failure(custom_id, 'Invalid or duplicate embedding record positions')]
|
|
127
117
|
end
|
|
128
118
|
|
|
129
|
-
|
|
119
|
+
billed = body unless endpoint == '/v1/embeddings'
|
|
120
|
+
parsed_batch_result(index, custom_id, billed, parser: batch_parser(endpoint)) do
|
|
121
|
+
parse_batch_body(body, endpoint:, model: data.fetch('model'), shape:)
|
|
122
|
+
end
|
|
130
123
|
end
|
|
131
124
|
|
|
132
125
|
def embedding_positions?(body)
|
|
@@ -137,8 +130,11 @@ module RubyLLM
|
|
|
137
130
|
def parse_batch_body(body, endpoint:, model:, shape:)
|
|
138
131
|
return parse_batch_embedding(body, model:, shape:) if endpoint == '/v1/embeddings'
|
|
139
132
|
|
|
140
|
-
|
|
141
|
-
|
|
133
|
+
batch_parser(endpoint).send(:parse_completion_body, body, raw: body)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def batch_parser(endpoint)
|
|
137
|
+
endpoint == '/v1/responses' ? OpenRouter::Responses.new(@provider) : self
|
|
142
138
|
end
|
|
143
139
|
|
|
144
140
|
def parse_batch_embedding(body, model:, shape:)
|
|
@@ -17,16 +17,12 @@ module RubyLLM
|
|
|
17
17
|
SERVER_TOOL_ALIASES
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
raise ArgumentError,
|
|
27
|
-
"OpenRouter MCP requires explicit require_approval: 'never'; approval events are not returned"
|
|
28
|
-
end
|
|
29
|
-
super
|
|
20
|
+
def document_urls?
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def provider_file_upload_options(_attachment)
|
|
25
|
+
{}
|
|
30
26
|
end
|
|
31
27
|
|
|
32
28
|
def parse_usage(usage)
|
|
@@ -5,17 +5,23 @@ module RubyLLM
|
|
|
5
5
|
module Perplexity
|
|
6
6
|
# Perplexity's Agent API, a dialect of the Responses API. A request names
|
|
7
7
|
# either a preset, which bundles a model, search settings, and
|
|
8
|
-
# instructions, or a model in provider/model form.
|
|
9
|
-
# model ids run the presets Perplexity recommends in their place.
|
|
8
|
+
# instructions, or a model in provider/model form.
|
|
10
9
|
class Agent < Responses
|
|
11
10
|
PRESETS = %w[fast low medium high xhigh wide-research].freeze
|
|
12
|
-
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
|
|
11
|
+
SERVER_TOOL_ALIASES = Responses::SERVER_TOOL_ALIASES.merge(
|
|
12
|
+
web_fetch: { tool: { type: 'fetch_url' } },
|
|
13
|
+
code_execution: { tool: { type: 'sandbox' } },
|
|
14
|
+
code_interpreter: { tool: { type: 'sandbox' } }
|
|
15
|
+
).freeze
|
|
16
|
+
SERVER_TOOL_USAGE_NAMES = {
|
|
17
|
+
'search_web' => 'web_search',
|
|
18
|
+
'fetch_url' => 'web_fetch',
|
|
19
|
+
'sandbox' => 'code_execution'
|
|
17
20
|
}.freeze
|
|
18
|
-
|
|
21
|
+
|
|
22
|
+
def server_tool_aliases
|
|
23
|
+
SERVER_TOOL_ALIASES
|
|
24
|
+
end
|
|
19
25
|
|
|
20
26
|
def completion_url
|
|
21
27
|
@provider.agent_url
|
|
@@ -24,19 +30,10 @@ module RubyLLM
|
|
|
24
30
|
def render_payload(messages, model:, max_output_tokens: nil, **)
|
|
25
31
|
max_output_tokens ||= default_max_output_tokens(model)
|
|
26
32
|
payload = super
|
|
27
|
-
|
|
28
|
-
return payload unless preset
|
|
33
|
+
return payload unless PRESETS.include?(model.id)
|
|
29
34
|
|
|
30
35
|
payload.delete(:model)
|
|
31
|
-
payload.merge(preset:
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def format_document(document)
|
|
35
|
-
raise UnsupportedAttachmentError, document.mime_type
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def supported_attachment?(attachment)
|
|
39
|
-
attachment.provider_file? || (%i[image text].include?(attachment.type) && super)
|
|
36
|
+
payload.merge(preset: model.id)
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
private
|
|
@@ -45,19 +42,6 @@ module RubyLLM
|
|
|
45
42
|
model.max_output_tokens || Anthropic::DEFAULT_MAX_OUTPUT_TOKENS if model.id.start_with?('anthropic/')
|
|
46
43
|
end
|
|
47
44
|
|
|
48
|
-
def preset_for(model_id)
|
|
49
|
-
return model_id if PRESETS.include?(model_id)
|
|
50
|
-
|
|
51
|
-
preset = SONAR_PRESETS[model_id]
|
|
52
|
-
if preset
|
|
53
|
-
RubyLLM.deprecator.warn(
|
|
54
|
-
"Perplexity retires Sonar on September 27, 2026. #{model_id} now runs the #{preset} " \
|
|
55
|
-
"Agent API preset; use model: \"#{preset}\" instead."
|
|
56
|
-
)
|
|
57
|
-
end
|
|
58
|
-
preset
|
|
59
|
-
end
|
|
60
|
-
|
|
61
45
|
def parse_citations(data, output, content)
|
|
62
46
|
citations = super
|
|
63
47
|
return citations if citations.any?
|
|
@@ -92,7 +76,7 @@ module RubyLLM
|
|
|
92
76
|
return super unless item['type'] == 'function_call'
|
|
93
77
|
|
|
94
78
|
chunk tool_calls: {
|
|
95
|
-
data['output_index'] => ToolCall.new(
|
|
79
|
+
data['output_index'] => ToolCall.new(provider_call_id: item['call_id'], name: item['name'],
|
|
96
80
|
arguments: +item['arguments'].to_s)
|
|
97
81
|
}
|
|
98
82
|
end
|
|
@@ -100,14 +84,14 @@ module RubyLLM
|
|
|
100
84
|
# Perplexity rejects its own search_results and fetch_url_results
|
|
101
85
|
# items as input, so a replayed turn keeps only messages, reasoning,
|
|
102
86
|
# and function calls.
|
|
103
|
-
def
|
|
87
|
+
def render_assistant_items(msg)
|
|
104
88
|
super.select do |item|
|
|
105
89
|
type = item[:type] || item['type']
|
|
106
90
|
type.nil? || Responses::Chat::CLIENT_OUTPUT_ITEM_TYPES.include?(type)
|
|
107
91
|
end
|
|
108
92
|
end
|
|
109
93
|
|
|
110
|
-
def
|
|
94
|
+
def provider_file_references?
|
|
111
95
|
false
|
|
112
96
|
end
|
|
113
97
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
module Perplexity
|
|
6
|
+
# Perplexity's Batch API, the OpenAI batch shape for Agent API requests.
|
|
7
|
+
module Batches
|
|
8
|
+
include Protocols::Responses::Batches
|
|
9
|
+
|
|
10
|
+
# The Idempotency-Key lets the connection retry the create after a 429,
|
|
11
|
+
# which arrives whenever the upload just used the 1 request per second
|
|
12
|
+
# that the batch and file endpoints allow at the lowest usage tier.
|
|
13
|
+
def create_batch(requests)
|
|
14
|
+
single_batch_model!(requests, @provider.slug)
|
|
15
|
+
validate_batch_requests!(requests)
|
|
16
|
+
|
|
17
|
+
payload = { input_file_id: upload_batch_file(requests), endpoint: batch_endpoint, completion_window: '24h' }
|
|
18
|
+
response = @connection.post(batch_create_url, payload) do |request|
|
|
19
|
+
request.headers['Idempotency-Key'] = SecureRandom.uuid
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
parse_batch_response(response.body)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def batch_create_url
|
|
28
|
+
'v1/batches'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def batch_url(id)
|
|
32
|
+
"v1/batches/#{id}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Protocols
|
|
5
5
|
module Perplexity
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
# Perplexity's two kinds of stored file: batch input and output under
|
|
7
|
+
# /v1/files, and files an Agent response generated, which belong to it
|
|
8
|
+
# and are named response_id/files/file_id.
|
|
9
|
+
class Files < Protocols::OpenAI::Files
|
|
10
|
+
STORED_FILE_ID = /\A[A-Za-z0-9_-]+\z/
|
|
11
|
+
RESPONSE_FILE_ID = %r{\A([A-Za-z0-9_-]+)/files/([A-Za-z0-9_-]+)\z}
|
|
12
|
+
private_constant :STORED_FILE_ID, :RESPONSE_FILE_ID
|
|
11
13
|
|
|
12
14
|
def find(resource_id)
|
|
13
15
|
response_id, file_id = split_resource_id(resource_id)
|
|
16
|
+
return super unless response_id
|
|
17
|
+
|
|
14
18
|
response = @connection.get("v1/agent/#{response_id}/files")
|
|
15
19
|
data = Array(response.body['data']).find { |item| item['id'] == file_id }
|
|
16
20
|
raise Error.new('Perplexity response file was not found', response:) unless data
|
|
@@ -31,16 +35,22 @@ module RubyLLM
|
|
|
31
35
|
|
|
32
36
|
private
|
|
33
37
|
|
|
38
|
+
def files_url
|
|
39
|
+
'v1/files'
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
def download_file_url(resource_id)
|
|
35
|
-
split_resource_id(resource_id)
|
|
36
|
-
"v1/agent/#{resource_id}/content"
|
|
43
|
+
split_resource_id(resource_id) ? "v1/agent/#{resource_id}/content" : super
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
def split_resource_id(resource_id)
|
|
40
|
-
|
|
47
|
+
id = resource_id.to_s
|
|
48
|
+
return if id.match?(STORED_FILE_ID)
|
|
49
|
+
|
|
50
|
+
match = id.match(RESPONSE_FILE_ID)
|
|
41
51
|
return match.captures if match
|
|
42
52
|
|
|
43
|
-
raise ArgumentError, 'Perplexity file IDs must
|
|
53
|
+
raise ArgumentError, 'Perplexity file IDs must be a file id or response_id/files/file_id'
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
end
|
|
@@ -7,24 +7,25 @@ module RubyLLM
|
|
|
7
7
|
module Approvals
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def render_tool_approval_response(tool_call, approved:)
|
|
11
|
-
[{ type: 'mcp_approval_response', approval_request_id: tool_call.
|
|
10
|
+
def render_tool_approval_response(tool_call, approved:, note: nil)
|
|
11
|
+
[{ type: 'mcp_approval_response', approval_request_id: tool_call.provider_call_id, approve: approved,
|
|
12
|
+
reason: note }.compact]
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def parse_tool_approvals(output, response: nil, finish_reason: nil)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[call.id, call]
|
|
16
|
+
calls = output.filter_map do |item|
|
|
17
|
+
next unless item['type'] == 'mcp_approval_request'
|
|
18
|
+
|
|
19
|
+
arguments = ToolCall.parse_arguments(item['arguments'], response:, finish_reason:)
|
|
20
|
+
ToolCall.new(provider_call_id: item.fetch('id'), name: item.fetch('name'), **arguments, remote: true)
|
|
21
21
|
end
|
|
22
|
+
ToolCall.index(calls, by: :provider_call_id)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def parse_pending_tool_calls(output, response: nil, finish_reason: nil)
|
|
25
|
-
calls = (parse_function_calls(output, response:, finish_reason:) || {})
|
|
26
|
-
|
|
27
|
-
calls unless calls.empty?
|
|
26
|
+
calls = (parse_function_calls(output, response:, finish_reason:) || {}).values +
|
|
27
|
+
parse_tool_approvals(output, response:, finish_reason:).values
|
|
28
|
+
ToolCall.index(calls, by: :provider_call_id) unless calls.empty?
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
end
|