ruby_llm 2.1.1 → 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 +160 -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 +515 -274
- 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 +1593 -331
- 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 +50 -24
- 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 +127 -106
- 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 +15 -15
- 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 +1 -7
- 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 +5 -14
- 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 +213 -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
|
@@ -44,12 +44,12 @@ module RubyLLM
|
|
|
44
44
|
RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def render_tool_calls(tool_calls)
|
|
48
48
|
return nil unless tool_calls&.any?
|
|
49
49
|
|
|
50
|
-
tool_calls.map do |
|
|
50
|
+
tool_calls.map do |tc|
|
|
51
51
|
call = {
|
|
52
|
-
id: tc.
|
|
52
|
+
id: tc.provider_call_id,
|
|
53
53
|
type: 'function',
|
|
54
54
|
function: {
|
|
55
55
|
name: tc.name,
|
|
@@ -65,39 +65,26 @@ module RubyLLM
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
def parse_tool_call_arguments(tool_call, response: nil, finish_reason: nil)
|
|
69
|
-
arguments = tool_call.dig('function', 'arguments')
|
|
70
|
-
|
|
71
|
-
if arguments.nil? || arguments.empty?
|
|
72
|
-
{}
|
|
73
|
-
else
|
|
74
|
-
JSON.parse(arguments)
|
|
75
|
-
end
|
|
76
|
-
rescue JSON::ParserError => e
|
|
77
|
-
raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
|
|
78
|
-
end
|
|
79
|
-
|
|
80
68
|
# Streaming deltas key by index so id-less argument fragments find
|
|
81
69
|
# their call even when a delta batches or interleaves several calls;
|
|
82
70
|
# complete responses key by id, which tool results join on.
|
|
83
71
|
def parse_tool_calls(tool_calls, parse_arguments: true, response: nil, finish_reason: nil, stream_keys: false)
|
|
84
72
|
return nil unless tool_calls&.any?
|
|
85
73
|
|
|
86
|
-
tool_calls.
|
|
74
|
+
calls = tool_calls.map do |tc|
|
|
87
75
|
[
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
id: tc['id'],
|
|
91
|
-
name: tc.dig('function', 'name'),
|
|
92
|
-
arguments: if parse_arguments
|
|
93
|
-
parse_tool_call_arguments(tc, response: response, finish_reason: finish_reason)
|
|
94
|
-
else
|
|
95
|
-
tc.dig('function', 'arguments')
|
|
96
|
-
end,
|
|
97
|
-
thought_signature: extract_tool_call_thought_signature(tc)
|
|
98
|
-
)
|
|
76
|
+
tc['index'] || tc['id'],
|
|
77
|
+
parse_tool_call(tc, parse_arguments:, response:, finish_reason:)
|
|
99
78
|
]
|
|
100
79
|
end
|
|
80
|
+
stream_keys ? calls.to_h : ToolCall.index(calls.map(&:last), by: :provider_call_id)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_tool_call(call, parse_arguments:, response:, finish_reason:)
|
|
84
|
+
arguments = call.dig('function', 'arguments')
|
|
85
|
+
arguments = parse_arguments ? ToolCall.parse_arguments(arguments, response:, finish_reason:) : { arguments: }
|
|
86
|
+
ToolCall.new(provider_call_id: call['id'], name: call.dig('function', 'name'), **arguments,
|
|
87
|
+
thought_signature: parse_tool_call_thought_signature(call))
|
|
101
88
|
end
|
|
102
89
|
|
|
103
90
|
def build_tool_choice(tool_choice)
|
|
@@ -114,7 +101,7 @@ module RubyLLM
|
|
|
114
101
|
end
|
|
115
102
|
end
|
|
116
103
|
|
|
117
|
-
def
|
|
104
|
+
def parse_tool_call_thought_signature(tool_call)
|
|
118
105
|
tool_call.dig('extra_content', 'google', 'thought_signature')
|
|
119
106
|
end
|
|
120
107
|
end
|
|
@@ -5,6 +5,13 @@ module RubyLLM
|
|
|
5
5
|
class ChatCompletions
|
|
6
6
|
# Audio transcription methods for the OpenAI API integration
|
|
7
7
|
module Transcription
|
|
8
|
+
STREAM_TYPES = {
|
|
9
|
+
'transcript.text.delta' => RubyLLM::TranscriptionChunk::DELTA,
|
|
10
|
+
'transcript.text.partial' => RubyLLM::TranscriptionChunk::PARTIAL,
|
|
11
|
+
'transcript.text.segment' => RubyLLM::TranscriptionChunk::SEGMENT,
|
|
12
|
+
'transcript.text.done' => RubyLLM::TranscriptionChunk::DONE
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
8
15
|
module_function
|
|
9
16
|
|
|
10
17
|
def transcription_url
|
|
@@ -44,7 +51,7 @@ module RubyLLM
|
|
|
44
51
|
return nil unless references
|
|
45
52
|
|
|
46
53
|
references.map do |ref|
|
|
47
|
-
Attachment.new(ref, config: @config).for_llm
|
|
54
|
+
(ref.is_a?(Attachment) ? ref : Attachment.new(ref, config: @config)).for_llm
|
|
48
55
|
end
|
|
49
56
|
end
|
|
50
57
|
|
|
@@ -74,7 +81,7 @@ module RubyLLM
|
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
def build_transcription_chunk(data)
|
|
77
|
-
type = data['type']
|
|
84
|
+
type = STREAM_TYPES[data['type']]
|
|
78
85
|
|
|
79
86
|
RubyLLM::TranscriptionChunk.new(
|
|
80
87
|
type: type,
|
|
@@ -96,6 +103,7 @@ module RubyLLM
|
|
|
96
103
|
language: data['language'],
|
|
97
104
|
duration: transcription_duration(usage),
|
|
98
105
|
segments: streamed_transcription_segments(chunks, data),
|
|
106
|
+
audio_seconds: billed_audio_seconds(usage),
|
|
99
107
|
reported_cost: reported_cost(usage),
|
|
100
108
|
**transcription_tokens(usage)
|
|
101
109
|
)
|
|
@@ -129,6 +137,7 @@ module RubyLLM
|
|
|
129
137
|
duration: data['duration'] || transcription_duration(usage),
|
|
130
138
|
segments: data['segments'],
|
|
131
139
|
words: data['words'],
|
|
140
|
+
audio_seconds: billed_audio_seconds(usage, data['duration']),
|
|
132
141
|
reported_cost: reported_cost(usage),
|
|
133
142
|
**transcription_tokens(usage)
|
|
134
143
|
)
|
|
@@ -144,6 +153,12 @@ module RubyLLM
|
|
|
144
153
|
def transcription_duration(usage)
|
|
145
154
|
usage['seconds']
|
|
146
155
|
end
|
|
156
|
+
|
|
157
|
+
def billed_audio_seconds(usage, duration = nil)
|
|
158
|
+
return usage['seconds'] if usage['type'] == 'duration'
|
|
159
|
+
|
|
160
|
+
duration if transcription_tokens(usage).values.all?(&:nil?)
|
|
161
|
+
end
|
|
147
162
|
end
|
|
148
163
|
end
|
|
149
164
|
end
|
|
@@ -4,6 +4,10 @@ module RubyLLM
|
|
|
4
4
|
module Protocols
|
|
5
5
|
# The OpenAI Chat Completions API — the lingua franca of LLM APIs.
|
|
6
6
|
class ChatCompletions < Protocol
|
|
7
|
+
# The endpoint paths a base URL of an OpenAI-compatible API must not
|
|
8
|
+
# end in, since RubyLLM adds them.
|
|
9
|
+
ENDPOINT_PATHS = %r{/(?:chat/completions|completions|responses|embeddings)\z}
|
|
10
|
+
|
|
7
11
|
include ChatCompletions::Chat
|
|
8
12
|
include ChatCompletions::Embeddings
|
|
9
13
|
include ChatCompletions::Models
|
|
@@ -38,7 +38,7 @@ module RubyLLM
|
|
|
38
38
|
return [] if data['output_dataset_id'].to_s.empty?
|
|
39
39
|
|
|
40
40
|
file = datasets.wait_for_validation(data.fetch('output_dataset_id'))
|
|
41
|
-
model =
|
|
41
|
+
model = batch_model(data.fetch('model'))
|
|
42
42
|
parser = self.class.new(@provider, model)
|
|
43
43
|
datasets.records(file).map { |row| parse_batch_result(row, parser:, model: model.id) }
|
|
44
44
|
end
|
|
@@ -49,6 +49,12 @@ module RubyLLM
|
|
|
49
49
|
@datasets ||= Cohere::Datasets.new(@provider)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def batch_model(id)
|
|
53
|
+
RubyLLM.models.find(id, provider: @provider.slug, config: @config)
|
|
54
|
+
rescue ModelNotFoundError
|
|
55
|
+
Model.default(id, @provider.slug)
|
|
56
|
+
end
|
|
57
|
+
|
|
52
58
|
def batch_data(id)
|
|
53
59
|
@connection.get("v2/batches/#{id}").body.fetch('batch')
|
|
54
60
|
end
|
|
@@ -80,13 +86,14 @@ module RubyLLM
|
|
|
80
86
|
body = row.fetch('body')
|
|
81
87
|
return [index, nil, batch_failure(custom_id, 'generation failed')] if body['finish_reason'] == 'ERROR'
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
billed = body.merge('model' => model) unless body['embeddings']
|
|
90
|
+
parsed_batch_result(index, custom_id, billed, parser:) do
|
|
91
|
+
if body['embeddings']
|
|
92
|
+
parser.send(:parse_embedding_response, Response.new(body), model:, text: shape == 'array' ? [] : nil)
|
|
93
|
+
else
|
|
94
|
+
parser.send(:parse_completion_body, body, raw: body)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
90
97
|
end
|
|
91
98
|
end
|
|
92
99
|
end
|
|
@@ -16,21 +16,21 @@ module RubyLLM
|
|
|
16
16
|
def normalize_finish_reason(reason)
|
|
17
17
|
return nil if reason.nil?
|
|
18
18
|
|
|
19
|
-
finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
|
|
19
|
+
finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def completion_url
|
|
23
23
|
'v2/chat'
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
27
26
|
def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
|
|
28
27
|
schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
|
|
29
28
|
warn_unsupported_citations(model) if citations && !model.supports?(:citations)
|
|
29
|
+
reject_caching_options(caching)
|
|
30
30
|
|
|
31
31
|
payload = {
|
|
32
32
|
model: model.id,
|
|
33
|
-
messages:
|
|
33
|
+
messages: render_messages(messages, citations: citations),
|
|
34
34
|
stream: stream
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -40,10 +40,18 @@ module RubyLLM
|
|
|
40
40
|
payload
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
def cache_option_names = []
|
|
44
|
+
|
|
45
|
+
def reject_caching_options(caching)
|
|
46
|
+
return unless caching.is_a?(Hash) && caching.any?
|
|
47
|
+
|
|
48
|
+
raise ArgumentError, "Cohere caches on its own and does not accept #{caching.keys.join(', ')} for caching"
|
|
49
|
+
end
|
|
50
|
+
|
|
43
51
|
def add_optional_fields(payload, messages, temperature:, max_output_tokens:, citations:, schema:)
|
|
44
52
|
payload[:temperature] = temperature unless temperature.nil?
|
|
45
53
|
payload[:max_tokens] = max_output_tokens unless max_output_tokens.nil?
|
|
46
|
-
payload[:documents] = Media.
|
|
54
|
+
payload[:documents] = Media.render_documents(messages) if citations && Media.documents?(messages)
|
|
47
55
|
payload[:response_format] = build_response_format(schema) if schema
|
|
48
56
|
end
|
|
49
57
|
|
|
@@ -81,28 +89,28 @@ module RubyLLM
|
|
|
81
89
|
payload[:thinking] = { type: 'enabled', token_budget: thinking.budget }.compact
|
|
82
90
|
end
|
|
83
91
|
|
|
84
|
-
def
|
|
85
|
-
messages.map { |msg|
|
|
92
|
+
def render_messages(messages, citations: false)
|
|
93
|
+
messages.map { |msg| render_message(msg, citations: citations) }
|
|
86
94
|
end
|
|
87
95
|
|
|
88
|
-
def
|
|
89
|
-
return Tools.
|
|
90
|
-
return
|
|
96
|
+
def render_message(msg, citations: false)
|
|
97
|
+
return Tools.render_tool_result(msg) if msg.tool_result?
|
|
98
|
+
return render_assistant_message(msg) if msg.role == :assistant
|
|
91
99
|
|
|
92
100
|
{
|
|
93
101
|
role: msg.role.to_s,
|
|
94
|
-
content: Media.
|
|
102
|
+
content: Media.render_content(msg.content, msg.attachments, citations: citations)
|
|
95
103
|
}
|
|
96
104
|
end
|
|
97
105
|
|
|
98
106
|
# Cohere returns tool_plan alongside tool calls, and RubyLLM surfaces
|
|
99
107
|
# it as thinking, but the newer models reject it on the way back in.
|
|
100
108
|
# It is optional in a request, so the plan stays out of the history.
|
|
101
|
-
def
|
|
109
|
+
def render_assistant_message(msg)
|
|
102
110
|
message = { role: 'assistant' }
|
|
103
|
-
content = Media.
|
|
111
|
+
content = Media.render_content(msg.content, msg.attachments)
|
|
104
112
|
message[:content] = content unless content.empty?
|
|
105
|
-
message[:tool_calls] = Tools.
|
|
113
|
+
message[:tool_calls] = Tools.render_tool_calls(msg.tool_calls) if msg.tool_call?
|
|
106
114
|
message
|
|
107
115
|
end
|
|
108
116
|
|
|
@@ -111,46 +119,53 @@ module RubyLLM
|
|
|
111
119
|
|
|
112
120
|
message_data = data['message'] || {}
|
|
113
121
|
blocks = Array(message_data['content'])
|
|
114
|
-
content, offsets =
|
|
122
|
+
content, offsets = parse_text(blocks)
|
|
115
123
|
finish_reason = normalize_finish_reason(data['finish_reason'])
|
|
116
124
|
|
|
117
125
|
Message.new(
|
|
118
126
|
role: :assistant,
|
|
119
127
|
content: content,
|
|
120
128
|
citations: parse_citations(message_data['citations'], offsets),
|
|
121
|
-
thinking: Thinking.build(text:
|
|
129
|
+
thinking: Thinking.build(text: parse_thinking(blocks, message_data)),
|
|
122
130
|
tool_calls: Tools.parse_tool_calls(message_data['tool_calls'], response: raw, finish_reason:),
|
|
123
131
|
finish_reason: finish_reason,
|
|
124
132
|
model: model&.id,
|
|
125
133
|
raw: raw,
|
|
126
|
-
**
|
|
134
|
+
**parse_completion_usage(data)
|
|
127
135
|
)
|
|
128
136
|
end
|
|
129
137
|
|
|
138
|
+
def parse_completion_usage(data)
|
|
139
|
+
usage_tokens(data['usage'] || {})
|
|
140
|
+
end
|
|
141
|
+
|
|
130
142
|
# Cohere answers a generation that failed on its side with an ERROR
|
|
131
143
|
# finish reason instead of an error status.
|
|
132
144
|
def raise_failed_generation(message, response: nil)
|
|
133
|
-
raise ServerError.new(
|
|
145
|
+
raise ServerError.new('The generation failed due to an internal error', response:) unless message
|
|
146
|
+
|
|
147
|
+
raise ServerError.new(provider_message: message, response:, redact: @config&.redact_error_messages)
|
|
134
148
|
end
|
|
135
149
|
|
|
136
|
-
# Cohere reports what
|
|
137
|
-
#
|
|
138
|
-
# not bill
|
|
150
|
+
# Cohere reports what it charges for under billed_units and what the
|
|
151
|
+
# model processed under tokens. The processed counts and cached_tokens
|
|
152
|
+
# include Cohere's own preamble, which it does not bill, so a reply's
|
|
153
|
+
# usage is its billed units.
|
|
139
154
|
def usage_tokens(usage)
|
|
140
155
|
tokens = usage['tokens'] || {}
|
|
141
156
|
billed = usage['billed_units'] || {}
|
|
142
157
|
|
|
143
158
|
{
|
|
144
|
-
input_tokens:
|
|
145
|
-
output_tokens:
|
|
146
|
-
|
|
159
|
+
input_tokens: billed['input_tokens'] || tokens['input_tokens'],
|
|
160
|
+
output_tokens: billed['output_tokens'] || tokens['output_tokens'],
|
|
161
|
+
thinking_tokens: tokens['reasoning_tokens']
|
|
147
162
|
}
|
|
148
163
|
end
|
|
149
164
|
|
|
150
165
|
# Returns the joined text of the response along with the offset each
|
|
151
166
|
# content block starts at, so citation spans resolve against the
|
|
152
167
|
# content string RubyLLM exposes.
|
|
153
|
-
def
|
|
168
|
+
def parse_text(blocks)
|
|
154
169
|
text = +''
|
|
155
170
|
offsets = {}
|
|
156
171
|
|
|
@@ -166,7 +181,7 @@ module RubyLLM
|
|
|
166
181
|
|
|
167
182
|
# The tool plan is the model's reasoning for a tool-calling turn, and
|
|
168
183
|
# is the only reasoning Cohere returns when thinking blocks are absent.
|
|
169
|
-
def
|
|
184
|
+
def parse_thinking(blocks, message_data)
|
|
170
185
|
thoughts = blocks.select { |block| block['type'] == 'thinking' }
|
|
171
186
|
.map { |block| block['thinking'] }.join
|
|
172
187
|
thoughts.empty? ? message_data['tool_plan'] : thoughts
|
|
@@ -191,6 +206,7 @@ module RubyLLM
|
|
|
191
206
|
text: data['text'],
|
|
192
207
|
start_index: start_index,
|
|
193
208
|
end_index: end_index,
|
|
209
|
+
source_id: tool_output_id(source),
|
|
194
210
|
source_index: source_index(source)
|
|
195
211
|
)
|
|
196
212
|
end
|
|
@@ -220,6 +236,12 @@ module RubyLLM
|
|
|
220
236
|
url if url.is_a?(String) && url.match?(%r{\Ahttps?://}i)
|
|
221
237
|
end
|
|
222
238
|
|
|
239
|
+
# Cohere cites a tool output by the id its document was sent with,
|
|
240
|
+
# so a search result comes back with its source_id or URL.
|
|
241
|
+
def tool_output_id(source)
|
|
242
|
+
source['id'] if source && source['type'] == 'tool'
|
|
243
|
+
end
|
|
244
|
+
|
|
223
245
|
# Documents RubyLLM sends, and those Cohere numbers itself, are
|
|
224
246
|
# identified as doc:N where N is the document's position.
|
|
225
247
|
def source_index(source)
|
|
@@ -5,10 +5,14 @@ module RubyLLM
|
|
|
5
5
|
class Cohere
|
|
6
6
|
# Cohere datasets for batch input and generated results.
|
|
7
7
|
class Datasets < Protocols::Files
|
|
8
|
+
def rejected_options(operation)
|
|
9
|
+
operation == :upload ? %i[expires_in uri] : super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
8
13
|
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
|
|
9
14
|
provider_options: {})
|
|
10
15
|
raise ArgumentError, 'Cohere datasets require purpose: with a dataset type' unless purpose
|
|
11
|
-
raise ArgumentError, 'Cohere datasets do not accept expires_in or uri' if expires_in || uri
|
|
12
16
|
|
|
13
17
|
attachment = file_attachment(file, filename:)
|
|
14
18
|
options = { name: attachment.filename, type: purpose, keep_original_file: true }
|
|
@@ -88,7 +92,7 @@ module RubyLLM
|
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
def download_part(url)
|
|
91
|
-
Transport::Connection.
|
|
95
|
+
Transport::Connection.download(url, @config)
|
|
92
96
|
end
|
|
93
97
|
|
|
94
98
|
def load_avro
|
|
@@ -13,9 +13,11 @@ module RubyLLM
|
|
|
13
13
|
'v2/embed'
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
def rejected_options(operation)
|
|
17
|
+
operation == :embed ? %i[title] : super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_embedding_payload(text, model:, dimensions:, task_type: nil, with: [], provider_options: {}, **)
|
|
19
21
|
image_only = with.any? && separate_image_embeddings?(model)
|
|
20
22
|
payload = {
|
|
21
23
|
model: model,
|
|
@@ -28,7 +30,7 @@ module RubyLLM
|
|
|
28
30
|
Support::Utils.deep_merge(payload, provider_options)
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def
|
|
33
|
+
def embedding_media?
|
|
32
34
|
true
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -63,7 +65,7 @@ module RubyLLM
|
|
|
63
65
|
|
|
64
66
|
raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
|
|
65
67
|
|
|
66
|
-
{ inputs: [{ content: Media.
|
|
68
|
+
{ inputs: [{ content: Media.render_content(text, attachments) }] }
|
|
67
69
|
end
|
|
68
70
|
end
|
|
69
71
|
end
|
|
@@ -18,32 +18,32 @@ module RubyLLM
|
|
|
18
18
|
# Cohere user messages accept only text and image_url blocks. Text
|
|
19
19
|
# attachments become citable documents when citations are on, so they
|
|
20
20
|
# are pulled out of the message and left out of the content blocks.
|
|
21
|
-
def
|
|
21
|
+
def render_content(content, attachments = [], citations: false)
|
|
22
22
|
parts = []
|
|
23
|
-
parts <<
|
|
23
|
+
parts << render_text(content) if content
|
|
24
24
|
|
|
25
25
|
attachments.each do |attachment|
|
|
26
26
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
27
27
|
|
|
28
28
|
case attachment.type
|
|
29
29
|
when :image
|
|
30
|
-
parts <<
|
|
30
|
+
parts << render_image(attachment)
|
|
31
31
|
when :text
|
|
32
|
-
parts <<
|
|
32
|
+
parts << render_text(attachment.for_llm) unless citations
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
parts
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def render_text(text)
|
|
40
40
|
{
|
|
41
41
|
type: 'text',
|
|
42
42
|
text: text
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def render_image(image)
|
|
47
47
|
{
|
|
48
48
|
type: 'image_url',
|
|
49
49
|
image_url: { url: image_source(image) }
|
|
@@ -58,7 +58,7 @@ module RubyLLM
|
|
|
58
58
|
|
|
59
59
|
# Text attachments across the conversation become the request's
|
|
60
60
|
# top-level documents array, which is what Cohere cites against.
|
|
61
|
-
def
|
|
61
|
+
def render_documents(messages)
|
|
62
62
|
index = -1
|
|
63
63
|
|
|
64
64
|
messages.flat_map do |message|
|
|
@@ -31,7 +31,8 @@ module RubyLLM
|
|
|
31
31
|
results: parse_rerank_results(data, documents),
|
|
32
32
|
model: model,
|
|
33
33
|
raw: data,
|
|
34
|
-
input_tokens: tokens['input_tokens'] || billed['input_tokens']
|
|
34
|
+
input_tokens: tokens['input_tokens'] || billed['input_tokens'],
|
|
35
|
+
search_units: billed['search_units']
|
|
35
36
|
)
|
|
36
37
|
end
|
|
37
38
|
|
|
@@ -11,23 +11,32 @@ module RubyLLM
|
|
|
11
11
|
completion_url
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def stream_response(...)
|
|
15
|
+
@stream_thinking = false
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def build_chunk(data)
|
|
15
20
|
type = data['type']
|
|
16
|
-
|
|
21
|
+
raise_failed_stream(data) if data.dig('delta', 'finish_reason') == 'ERROR'
|
|
17
22
|
track_content_lengths(data, type)
|
|
18
23
|
|
|
19
24
|
Chunk.new(
|
|
20
25
|
role: :assistant,
|
|
21
26
|
model: model&.id,
|
|
22
|
-
content:
|
|
23
|
-
citations:
|
|
24
|
-
thinking: Thinking.build(text:
|
|
27
|
+
content: parse_content_delta(data, type),
|
|
28
|
+
citations: parse_citations_delta(data, type),
|
|
29
|
+
thinking: Thinking.build(text: parse_thinking_delta(data, type)),
|
|
25
30
|
tool_calls: extract_tool_calls(data, type),
|
|
26
31
|
finish_reason: normalize_finish_reason(data.dig('delta', 'finish_reason')),
|
|
27
32
|
**stream_usage(data.dig('delta', 'usage'))
|
|
28
33
|
)
|
|
29
34
|
end
|
|
30
35
|
|
|
36
|
+
def raise_failed_stream(data)
|
|
37
|
+
raise_failed_generation(data.dig('delta', 'error'), response: build_stream_error_response(data, nil, 500))
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
# Citation spans are offsets into their own content block, so the
|
|
32
41
|
# running length of every earlier text block is what turns them into
|
|
33
42
|
# offsets into the accumulated response content.
|
|
@@ -47,18 +56,24 @@ module RubyLLM
|
|
|
47
56
|
@stream_content_lengths.sum { |block_index, length| block_index < index ? length : 0 }
|
|
48
57
|
end
|
|
49
58
|
|
|
50
|
-
def
|
|
59
|
+
def parse_content_delta(data, type)
|
|
51
60
|
data.dig('delta', 'message', 'content', 'text') if type == 'content-delta'
|
|
52
61
|
end
|
|
53
62
|
|
|
54
|
-
def
|
|
63
|
+
def parse_thinking_block_delta(data)
|
|
64
|
+
thinking = data.dig('delta', 'message', 'content', 'thinking')
|
|
65
|
+
@stream_thinking = true if thinking
|
|
66
|
+
thinking
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_thinking_delta(data, type)
|
|
55
70
|
case type
|
|
56
|
-
when 'content-delta' then data
|
|
57
|
-
when 'tool-plan-delta' then data.dig('delta', 'message', 'tool_plan')
|
|
71
|
+
when 'content-delta' then parse_thinking_block_delta(data)
|
|
72
|
+
when 'tool-plan-delta' then data.dig('delta', 'message', 'tool_plan') unless @stream_thinking
|
|
58
73
|
end
|
|
59
74
|
end
|
|
60
75
|
|
|
61
|
-
def
|
|
76
|
+
def parse_citations_delta(data, type)
|
|
62
77
|
return nil unless type == 'citation-start'
|
|
63
78
|
|
|
64
79
|
citation = data.dig('delta', 'message', 'citations')
|
|
@@ -81,7 +96,7 @@ module RubyLLM
|
|
|
81
96
|
|
|
82
97
|
{
|
|
83
98
|
data['index'] => ToolCall.new(
|
|
84
|
-
|
|
99
|
+
provider_call_id: tool_call['id'],
|
|
85
100
|
name: tool_call.dig('function', 'name'),
|
|
86
101
|
arguments: tool_call.dig('function', 'arguments').to_s
|
|
87
102
|
)
|
|
@@ -92,7 +107,7 @@ module RubyLLM
|
|
|
92
107
|
arguments = data.dig('delta', 'message', 'tool_calls', 'function', 'arguments')
|
|
93
108
|
return nil unless arguments
|
|
94
109
|
|
|
95
|
-
{ data['index'] => ToolCall.new(
|
|
110
|
+
{ data['index'] => ToolCall.new(name: nil, arguments: arguments) }
|
|
96
111
|
end
|
|
97
112
|
|
|
98
113
|
def stream_usage(usage)
|
|
@@ -55,10 +55,10 @@ module RubyLLM
|
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
def
|
|
59
|
-
tool_calls.map do |
|
|
58
|
+
def render_tool_calls(tool_calls)
|
|
59
|
+
tool_calls.map do |tool_call|
|
|
60
60
|
{
|
|
61
|
-
id: tool_call.
|
|
61
|
+
id: tool_call.provider_call_id,
|
|
62
62
|
type: 'function',
|
|
63
63
|
function: {
|
|
64
64
|
name: tool_call.name,
|
|
@@ -68,11 +68,11 @@ module RubyLLM
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def
|
|
71
|
+
def render_tool_result(msg)
|
|
72
72
|
{
|
|
73
73
|
role: 'tool',
|
|
74
|
-
tool_call_id: msg.
|
|
75
|
-
content:
|
|
74
|
+
tool_call_id: msg.provider_call_id,
|
|
75
|
+
content: render_tool_result_content(msg)
|
|
76
76
|
}
|
|
77
77
|
end
|
|
78
78
|
|
|
@@ -84,15 +84,15 @@ module RubyLLM
|
|
|
84
84
|
# cites tool output against. A tool that attaches files sends its
|
|
85
85
|
# text and each attachment as blocks, so the model reads what the
|
|
86
86
|
# tool fetched rather than only the sentence describing it.
|
|
87
|
-
def
|
|
87
|
+
def render_tool_result_content(msg)
|
|
88
88
|
search_results = RubyLLM::SearchResults.from_content(msg.content)
|
|
89
89
|
return document_blocks(search_results) if search_results
|
|
90
90
|
return msg.content.to_s if msg.attachments.empty?
|
|
91
91
|
|
|
92
|
-
[Media.
|
|
92
|
+
[Media.render_text(msg.content.to_s)] + msg.attachments.map do |attachment|
|
|
93
93
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_message_attachment?(msg, attachment)
|
|
94
94
|
|
|
95
|
-
Media.
|
|
95
|
+
Media.render_text(attachment.for_llm)
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
@@ -101,7 +101,7 @@ module RubyLLM
|
|
|
101
101
|
{
|
|
102
102
|
type: 'document',
|
|
103
103
|
document: {
|
|
104
|
-
id: result[:url] || "tool:#{index}",
|
|
104
|
+
id: result[:source_id] || result[:url] || "tool:#{index}",
|
|
105
105
|
data: { title: result[:title], text: result[:text], url: result[:url] }.compact
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -111,24 +111,13 @@ module RubyLLM
|
|
|
111
111
|
def parse_tool_calls(tool_calls, response: nil, finish_reason: nil)
|
|
112
112
|
return nil unless tool_calls&.any?
|
|
113
113
|
|
|
114
|
-
tool_calls.
|
|
115
|
-
|
|
116
|
-
tool_call['id'],
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
]
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def parse_arguments(arguments, response, finish_reason)
|
|
127
|
-
return {} if arguments.nil? || arguments.empty?
|
|
128
|
-
|
|
129
|
-
JSON.parse(arguments)
|
|
130
|
-
rescue JSON::ParserError => e
|
|
131
|
-
raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
|
|
114
|
+
ToolCall.index(tool_calls.map do |tool_call|
|
|
115
|
+
ToolCall.new(
|
|
116
|
+
provider_call_id: tool_call['id'],
|
|
117
|
+
name: tool_call.dig('function', 'name'),
|
|
118
|
+
**ToolCall.parse_arguments(tool_call.dig('function', 'arguments'), response:, finish_reason:)
|
|
119
|
+
)
|
|
120
|
+
end, by: :provider_call_id)
|
|
132
121
|
end
|
|
133
122
|
end
|
|
134
123
|
end
|