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
|
@@ -21,37 +21,37 @@ module RubyLLM
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def render_content(content, attachments = [], used_document_names: nil, citations: false)
|
|
25
25
|
used_document_names ||= {}
|
|
26
26
|
blocks = []
|
|
27
27
|
blocks << { text: content } unless content.nil? || content.empty?
|
|
28
28
|
attachments.each do |attachment|
|
|
29
|
-
blocks <<
|
|
29
|
+
blocks << render_attachment(attachment, used_document_names:, citations:)
|
|
30
30
|
end
|
|
31
31
|
blocks
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def
|
|
34
|
+
def render_attachment(attachment, used_document_names:, citations: false)
|
|
35
35
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
36
36
|
|
|
37
37
|
if attachment.provider_file?
|
|
38
|
-
return
|
|
38
|
+
return render_provider_file_attachment(attachment, used_document_names:, citations:)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
case attachment.type
|
|
42
42
|
when :image
|
|
43
|
-
|
|
43
|
+
render_image_attachment(attachment)
|
|
44
44
|
when :audio
|
|
45
|
-
|
|
45
|
+
render_audio_attachment(attachment)
|
|
46
46
|
when :video
|
|
47
|
-
|
|
47
|
+
render_video_attachment(attachment)
|
|
48
48
|
when :pdf, :document
|
|
49
|
-
|
|
49
|
+
render_document_attachment(attachment, used_document_names:, citations:)
|
|
50
50
|
when :text
|
|
51
51
|
if citations
|
|
52
|
-
|
|
52
|
+
render_text_document_attachment(attachment, used_document_names:)
|
|
53
53
|
else
|
|
54
|
-
|
|
54
|
+
render_text_attachment(attachment)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -71,7 +71,7 @@ module RubyLLM
|
|
|
71
71
|
'3gpp' => 'three_gp'
|
|
72
72
|
}.freeze
|
|
73
73
|
|
|
74
|
-
def
|
|
74
|
+
def render_image_attachment(attachment)
|
|
75
75
|
{
|
|
76
76
|
image: {
|
|
77
77
|
format: attachment.format,
|
|
@@ -82,7 +82,7 @@ module RubyLLM
|
|
|
82
82
|
}
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def
|
|
85
|
+
def render_audio_attachment(attachment)
|
|
86
86
|
{
|
|
87
87
|
audio: {
|
|
88
88
|
format: media_format(attachment, AUDIO_FORMAT_ALIASES, SUPPORTED_AUDIO_FORMATS),
|
|
@@ -93,7 +93,7 @@ module RubyLLM
|
|
|
93
93
|
}
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
def
|
|
96
|
+
def render_video_attachment(attachment)
|
|
97
97
|
{
|
|
98
98
|
video: {
|
|
99
99
|
format: media_format(attachment, VIDEO_FORMAT_ALIASES, SUPPORTED_VIDEO_FORMATS),
|
|
@@ -112,11 +112,11 @@ module RubyLLM
|
|
|
112
112
|
format
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def
|
|
115
|
+
def render_text_attachment(attachment)
|
|
116
116
|
{ text: attachment.for_llm }
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def
|
|
119
|
+
def render_document_attachment(attachment, used_document_names:, citations: false)
|
|
120
120
|
format = document_format(attachment)
|
|
121
121
|
|
|
122
122
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_document_format?(attachment)
|
|
@@ -136,7 +136,7 @@ module RubyLLM
|
|
|
136
136
|
# Text attachments become citable documents when citations are
|
|
137
137
|
# enabled. Bedrock rejects text-format documents sent as bytes when
|
|
138
138
|
# citations are on, so the text rides in the source's text member.
|
|
139
|
-
def
|
|
139
|
+
def render_text_document_attachment(attachment, used_document_names:)
|
|
140
140
|
document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
|
|
141
141
|
{
|
|
142
142
|
document: {
|
|
@@ -148,7 +148,7 @@ module RubyLLM
|
|
|
148
148
|
}
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
def
|
|
151
|
+
def render_provider_file_attachment(attachment, used_document_names:, citations: false)
|
|
152
152
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_document_format?(attachment)
|
|
153
153
|
|
|
154
154
|
document_name = unique_document_name(sanitize_document_name(attachment.filename), used_document_names)
|
|
@@ -180,11 +180,13 @@ module RubyLLM
|
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
def unique_document_name(base_name, used_names)
|
|
183
|
-
|
|
184
|
-
used_names
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
name = base_name
|
|
184
|
+
while used_names.key?(name)
|
|
185
|
+
used_names[base_name] += 1
|
|
186
|
+
name = "#{base_name}_#{used_names[base_name]}"
|
|
187
|
+
end
|
|
188
|
+
used_names[name] = 1
|
|
189
|
+
name
|
|
188
190
|
end
|
|
189
191
|
end
|
|
190
192
|
end
|
|
@@ -14,17 +14,17 @@ module RubyLLM
|
|
|
14
14
|
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def render_reasoning_fields(thinking, model, max_output_tokens = nil)
|
|
18
18
|
return nil unless thinking&.enabled?
|
|
19
|
-
return
|
|
19
|
+
return render_nova_reasoning_fields(thinking, model) if nova_model?(model)
|
|
20
20
|
return disabled_reasoning_fields(model) if thinking.enabled == false
|
|
21
|
-
return
|
|
21
|
+
return render_adaptive_reasoning_fields(thinking) if adaptive_thinking?(thinking, model)
|
|
22
22
|
|
|
23
23
|
effort = thinking.effort.to_s
|
|
24
24
|
budget = reasoning_budget(thinking, effort, model, max_output_tokens)
|
|
25
25
|
return { reasoning_config: { type: 'enabled', budget_tokens: budget } } if budget
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
render_effort_fields(effort, model) unless effort.empty?
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def disabled_reasoning_fields(model)
|
|
@@ -42,7 +42,7 @@ module RubyLLM
|
|
|
42
42
|
|
|
43
43
|
# Models that publish a reasoning_config enum, such as OpenAI's GPT
|
|
44
44
|
# models, take the effort as its value and reject reasoning_effort.
|
|
45
|
-
def
|
|
45
|
+
def render_effort_fields(effort, model)
|
|
46
46
|
return { reasoning_config: effort } if reasoning_config_schema(model)
|
|
47
47
|
|
|
48
48
|
{ reasoning_effort: effort } unless effort == 'none'
|
|
@@ -52,14 +52,14 @@ module RubyLLM
|
|
|
52
52
|
# adaptively, and reject a reasoning_config that enables a budget.
|
|
53
53
|
def adaptive_thinking?(thinking, model)
|
|
54
54
|
return false unless thinking.budget.nil?
|
|
55
|
-
return false unless
|
|
55
|
+
return false unless claude_model?(model)
|
|
56
56
|
|
|
57
57
|
registered = registered_model(model)
|
|
58
58
|
!registered.nil? && !registered.reasoning_option(:effort).nil? &&
|
|
59
59
|
registered.reasoning_option(:budget_tokens).nil?
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def
|
|
62
|
+
def render_adaptive_reasoning_fields(thinking)
|
|
63
63
|
effort = thinking.effort.to_s
|
|
64
64
|
return nil if effort == 'none'
|
|
65
65
|
return { thinking: { type: 'adaptive' } } if effort.empty?
|
|
@@ -79,13 +79,17 @@ module RubyLLM
|
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def claude_model?(model)
|
|
83
|
+
Chat.foundation_model_id(model&.id).start_with?('anthropic.claude')
|
|
84
|
+
end
|
|
85
|
+
|
|
82
86
|
def nova_model?(model)
|
|
83
87
|
Chat.foundation_model_id(model&.id).start_with?('amazon.nova')
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
# Nova 2 takes reasoningConfig with an effort level instead of a token budget,
|
|
87
91
|
# and rejects an enabled reasoningConfig that names no effort.
|
|
88
|
-
def
|
|
92
|
+
def render_nova_reasoning_fields(thinking, model)
|
|
89
93
|
if thinking.budget
|
|
90
94
|
raise ArgumentError,
|
|
91
95
|
"#{model&.id} takes a reasoning effort, not a token budget of #{thinking.budget}. " \
|
|
@@ -103,12 +107,22 @@ module RubyLLM
|
|
|
103
107
|
|
|
104
108
|
def reasoning_budget(thinking, effort, model, max_output_tokens)
|
|
105
109
|
return thinking.budget if thinking.budget.is_a?(Integer)
|
|
106
|
-
return nil if effort
|
|
110
|
+
return nil if effort == 'none'
|
|
111
|
+
return toggle_budget(thinking, model) if effort.empty?
|
|
107
112
|
|
|
108
113
|
schema = reasoning_budget_schema(model)
|
|
109
114
|
schema && effort_budget_tokens(effort, schema, max_output_tokens)
|
|
110
115
|
end
|
|
111
116
|
|
|
117
|
+
# Claude generations that take a budget have no bare switch, so
|
|
118
|
+
# turning thinking on sends the smallest budget the model accepts.
|
|
119
|
+
def toggle_budget(thinking, model)
|
|
120
|
+
return nil unless thinking.enabled == true && claude_model?(model)
|
|
121
|
+
|
|
122
|
+
minimum = registered_model(model)&.reasoning_option(:budget_tokens)&.fetch(:min, nil)
|
|
123
|
+
[minimum, MINIMUM_BUDGET_TOKENS].max if minimum.is_a?(Integer)
|
|
124
|
+
end
|
|
125
|
+
|
|
112
126
|
def reasoning_budget_schema(model)
|
|
113
127
|
request_fields_schema(model) { |schema| schema.dig(:reasoningConfig, :budgetTokens) }
|
|
114
128
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'base64'
|
|
4
|
-
require 'faraday'
|
|
5
3
|
require 'json'
|
|
6
4
|
|
|
7
5
|
module RubyLLM
|
|
@@ -9,7 +7,7 @@ module RubyLLM
|
|
|
9
7
|
class Converse
|
|
10
8
|
# Streaming implementation for Bedrock ConverseStream (AWS Event Stream).
|
|
11
9
|
module Streaming
|
|
12
|
-
|
|
10
|
+
include Protocols::Bedrock::EventStream
|
|
13
11
|
|
|
14
12
|
private
|
|
15
13
|
|
|
@@ -19,28 +17,14 @@ module RubyLLM
|
|
|
19
17
|
|
|
20
18
|
def stream_response(payload, additional_headers = {}, &block)
|
|
21
19
|
@thinking_stream = ThinkingStream.new
|
|
20
|
+
@stream_text_lengths = Hash.new(0)
|
|
21
|
+
@stream_citations = {}
|
|
22
22
|
accumulator = RubyLLM::Protocol::StreamAccumulator.new
|
|
23
|
-
decoder = event_stream_decoder
|
|
24
|
-
body = JSON.generate(payload)
|
|
25
|
-
progress = {}
|
|
26
|
-
|
|
27
|
-
faraday_v1 = Faraday::VERSION.start_with?('1')
|
|
28
|
-
on_data = RubyLLM::Protocol::Streaming::FaradayHandlers.build(
|
|
29
|
-
faraday_v1: faraday_v1,
|
|
30
|
-
on_chunk: ->(chunk, _env) { parse_stream_chunk(decoder, chunk, accumulator, progress, &block) },
|
|
31
|
-
on_failed_response: ->(chunk, env) { handle_failed_stream(chunk, env) }
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
response = @connection.post(stream_url, body, usage: @usage_tracker, stream: true) do |req|
|
|
35
|
-
req.headers.merge!(additional_headers) unless additional_headers.empty?
|
|
36
|
-
req.headers['Accept'] = 'application/vnd.amazon.eventstream'
|
|
37
|
-
(req.options.context ||= {})[RubyLLM::Transport::Connection::STREAM_PROGRESS_KEY] = progress
|
|
38
23
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
24
|
+
response = stream_events(stream_url, JSON.generate(payload), additional_headers) do |event|
|
|
25
|
+
chunk = build_chunk(event)
|
|
26
|
+
accumulator.add(chunk)
|
|
27
|
+
block.call(chunk)
|
|
44
28
|
end
|
|
45
29
|
|
|
46
30
|
message = accumulator.to_message(response)
|
|
@@ -48,139 +32,6 @@ module RubyLLM
|
|
|
48
32
|
message
|
|
49
33
|
end
|
|
50
34
|
|
|
51
|
-
def event_stream_decoder
|
|
52
|
-
require 'aws-eventstream'
|
|
53
|
-
Aws::EventStream::Decoder.new
|
|
54
|
-
rescue LoadError
|
|
55
|
-
raise Error,
|
|
56
|
-
'The aws-eventstream gem is required for Bedrock streaming. ' \
|
|
57
|
-
'Please add it to your Gemfile: gem "aws-eventstream"'
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# The error body arrives in as many reads as the adapter hands out, so
|
|
61
|
-
# it accumulates on the env, which ErrorMiddleware clears per attempt.
|
|
62
|
-
def handle_failed_stream(chunk, env)
|
|
63
|
-
buffer = failed_stream_buffer(env)
|
|
64
|
-
buffer << chunk
|
|
65
|
-
error_response = env.merge(body: JSON.parse(buffer))
|
|
66
|
-
Transport::ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
67
|
-
rescue JSON::ParserError
|
|
68
|
-
RubyLLM.logger.debug { "Accumulating Bedrock stream error chunk: #{chunk}" }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def failed_stream_buffer(env)
|
|
72
|
-
(env[:streaming_state] ||= RubyLLM::Protocol::Streaming::StreamState.new).buffer
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def parse_stream_chunk(decoder, raw_chunk, accumulator, progress)
|
|
76
|
-
handle_non_eventstream_error_chunk(raw_chunk)
|
|
77
|
-
|
|
78
|
-
decode_events(decoder, raw_chunk).each do |event|
|
|
79
|
-
chunk = build_chunk(event)
|
|
80
|
-
next unless chunk
|
|
81
|
-
|
|
82
|
-
accumulator.add(chunk)
|
|
83
|
-
progress[:started] = true
|
|
84
|
-
yield chunk
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def handle_non_eventstream_error_chunk(raw_chunk)
|
|
89
|
-
text = raw_chunk.to_s
|
|
90
|
-
|
|
91
|
-
if text.start_with?('event: error')
|
|
92
|
-
payload = text.lines.find { |line| line.start_with?('data:') }&.delete_prefix('data:')&.strip
|
|
93
|
-
raise_streaming_chunk_error(payload) if payload
|
|
94
|
-
return
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
return unless text.lstrip.start_with?('{') && text.include?('"error"')
|
|
98
|
-
|
|
99
|
-
raise_streaming_chunk_error(text)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def raise_streaming_chunk_error(payload)
|
|
103
|
-
parsed = JSON.parse(payload)
|
|
104
|
-
message = parsed.dig('error', 'message') || parsed['message'] || 'Bedrock streaming error'
|
|
105
|
-
response = ErrorResponse.new({ 'message' => message }, 500)
|
|
106
|
-
Transport::ErrorMiddleware.parse_error(provider: self, response: response)
|
|
107
|
-
rescue JSON::ParserError
|
|
108
|
-
nil
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def decode_events(decoder, raw_chunk)
|
|
112
|
-
events = []
|
|
113
|
-
message, eof = decoder.decode_chunk(raw_chunk)
|
|
114
|
-
|
|
115
|
-
while message
|
|
116
|
-
event = decode_event(message)
|
|
117
|
-
if event && RubyLLM.config.log_stream_debug
|
|
118
|
-
RubyLLM.logger.debug do
|
|
119
|
-
"Bedrock stream event keys: #{event.keys}"
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
events << event if event
|
|
123
|
-
break if eof
|
|
124
|
-
|
|
125
|
-
message, eof = decoder.decode_chunk
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
events
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
# ConverseStream delivers modeled errors as a frame after the 200, with
|
|
132
|
-
# the type in the frame headers and a bare message as the payload. Rewrap
|
|
133
|
-
# it under the exception name so raise_stream_error classifies it instead
|
|
134
|
-
# of the frame passing for an empty chunk.
|
|
135
|
-
def decode_event(message)
|
|
136
|
-
return decode_error_frame(message.headers) if stream_error_frame?(message)
|
|
137
|
-
|
|
138
|
-
exception_type = stream_exception_type(message)
|
|
139
|
-
payload = message.payload.read
|
|
140
|
-
event = decode_event_payload(payload)
|
|
141
|
-
return event unless exception_type
|
|
142
|
-
|
|
143
|
-
{ exception_type => event || { 'message' => payload.to_s } }
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Failures the API does not model arrive as an error frame instead,
|
|
147
|
-
# with the code and message in the headers and nothing in the payload.
|
|
148
|
-
def stream_error_frame?(message)
|
|
149
|
-
event_header(message.headers, ':message-type') == 'error'
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
def decode_error_frame(headers)
|
|
153
|
-
detail = [event_header(headers, ':error-code'), event_header(headers, ':error-message')].compact.join(': ')
|
|
154
|
-
event = { 'type' => 'error' }
|
|
155
|
-
event['error'] = { 'message' => detail } unless detail.empty?
|
|
156
|
-
event
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def stream_exception_type(message)
|
|
160
|
-
headers = message.headers
|
|
161
|
-
return nil unless event_header(headers, ':message-type') == 'exception'
|
|
162
|
-
|
|
163
|
-
event_header(headers, ':exception-type') || 'streamingException'
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def event_header(headers, name)
|
|
167
|
-
header = headers[name]
|
|
168
|
-
header.respond_to?(:value) ? header.value : header
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def decode_event_payload(payload)
|
|
172
|
-
outer = JSON.parse(payload)
|
|
173
|
-
|
|
174
|
-
if outer['bytes'].is_a?(String)
|
|
175
|
-
JSON.parse(Base64.decode64(outer['bytes']))
|
|
176
|
-
else
|
|
177
|
-
outer
|
|
178
|
-
end
|
|
179
|
-
rescue JSON::ParserError => e
|
|
180
|
-
RubyLLM.logger.debug { "Failed to decode Bedrock stream event payload: #{e.message}" }
|
|
181
|
-
nil
|
|
182
|
-
end
|
|
183
|
-
|
|
184
35
|
def build_chunk(event)
|
|
185
36
|
raise_stream_error(event) if stream_error_event?(event)
|
|
186
37
|
|
|
@@ -189,22 +40,22 @@ module RubyLLM
|
|
|
189
40
|
Chunk.new(
|
|
190
41
|
role: :assistant,
|
|
191
42
|
model: event['modelId'] || @model&.id,
|
|
192
|
-
content:
|
|
193
|
-
citations:
|
|
43
|
+
content: parse_content_delta(event),
|
|
44
|
+
citations: parse_block_citations(event),
|
|
194
45
|
thinking: Thinking.build(
|
|
195
|
-
text:
|
|
196
|
-
signature:
|
|
46
|
+
text: parse_thinking_delta(event),
|
|
47
|
+
signature: parse_thinking_signature(event)
|
|
197
48
|
),
|
|
198
49
|
raw_reasoning: streamed_thinking_blocks(event),
|
|
199
50
|
tool_calls: extract_tool_calls(event),
|
|
200
|
-
server_tool_calls:
|
|
201
|
-
input_tokens:
|
|
202
|
-
output_tokens:
|
|
203
|
-
cache_read_tokens:
|
|
204
|
-
cache_write_tokens:
|
|
51
|
+
server_tool_calls: parse_server_tool_call_events(event),
|
|
52
|
+
input_tokens: parse_input_tokens(metadata_usage, usage),
|
|
53
|
+
output_tokens: parse_output_tokens(metadata_usage, usage),
|
|
54
|
+
cache_read_tokens: parse_cache_read_tokens(metadata_usage, usage),
|
|
55
|
+
cache_write_tokens: parse_cache_write_tokens(metadata_usage, usage),
|
|
205
56
|
cache_write_tokens_by_ttl: Chat.cache_write_by_ttl(metadata_usage) || Chat.cache_write_by_ttl(usage),
|
|
206
|
-
thinking_tokens:
|
|
207
|
-
finish_reason:
|
|
57
|
+
thinking_tokens: parse_reasoning_tokens(metadata_usage, usage),
|
|
58
|
+
finish_reason: parse_finish_reason(event)
|
|
208
59
|
)
|
|
209
60
|
end
|
|
210
61
|
|
|
@@ -214,7 +65,7 @@ module RubyLLM
|
|
|
214
65
|
@thinking_stream.raw_reasoning if event.key?('messageStop') || event.key?('stopReason')
|
|
215
66
|
end
|
|
216
67
|
|
|
217
|
-
def
|
|
68
|
+
def parse_finish_reason(event)
|
|
218
69
|
normalize_finish_reason(event.dig('messageStop', 'stopReason') || event['stopReason'])
|
|
219
70
|
end
|
|
220
71
|
|
|
@@ -225,69 +76,66 @@ module RubyLLM
|
|
|
225
76
|
]
|
|
226
77
|
end
|
|
227
78
|
|
|
228
|
-
def
|
|
79
|
+
def parse_input_tokens(metadata_usage, usage)
|
|
229
80
|
bedrock_usage = metadata_usage['inputTokens'] ? metadata_usage : usage
|
|
230
81
|
Chat.input_tokens(bedrock_usage) if bedrock_usage['inputTokens']
|
|
231
82
|
end
|
|
232
83
|
|
|
233
|
-
def
|
|
84
|
+
def parse_output_tokens(metadata_usage, usage)
|
|
234
85
|
metadata_usage['outputTokens'] || usage['outputTokens']
|
|
235
86
|
end
|
|
236
87
|
|
|
237
|
-
def
|
|
88
|
+
def parse_cache_read_tokens(metadata_usage, usage)
|
|
238
89
|
metadata_usage['cacheReadInputTokens'] || usage['cacheReadInputTokens']
|
|
239
90
|
end
|
|
240
91
|
|
|
241
|
-
def
|
|
92
|
+
def parse_cache_write_tokens(metadata_usage, usage)
|
|
242
93
|
metadata_usage['cacheWriteInputTokens'] || usage['cacheWriteInputTokens']
|
|
243
94
|
end
|
|
244
95
|
|
|
245
|
-
def
|
|
96
|
+
def parse_reasoning_tokens(metadata_usage, usage)
|
|
246
97
|
Chat.reasoning_tokens(metadata_usage) || Chat.reasoning_tokens(usage)
|
|
247
98
|
end
|
|
248
99
|
|
|
249
|
-
def
|
|
250
|
-
event
|
|
100
|
+
def parse_content_delta(event)
|
|
101
|
+
normalized_delta(event)['text']
|
|
251
102
|
end
|
|
252
103
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
104
|
+
# A citation delta arrives before the text it supports, so a block's
|
|
105
|
+
# citations wait for the block to stop and take its span of the response.
|
|
106
|
+
# A block still open when the message stops gives up what it has.
|
|
107
|
+
def parse_block_citations(event)
|
|
108
|
+
track_citation_block(event)
|
|
109
|
+
finished_citation_blocks(event).flat_map { |index| take_block_citations(index) }
|
|
110
|
+
end
|
|
260
111
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
response = ErrorResponse.new({ 'message' => message }, status)
|
|
273
|
-
Transport::ErrorMiddleware.parse_error(provider: self, response: response)
|
|
112
|
+
def track_citation_block(event)
|
|
113
|
+
@stream_text_lengths ||= Hash.new(0)
|
|
114
|
+
@stream_citations ||= {}
|
|
115
|
+
index = event_block_index(event)
|
|
116
|
+
return unless index
|
|
117
|
+
|
|
118
|
+
delta = normalized_delta(event)
|
|
119
|
+
@stream_text_lengths[index] += delta['text'].length if delta['text'].is_a?(String)
|
|
120
|
+
(@stream_citations[index] ||= []) << delta['citation'] if delta['citation'].is_a?(Hash)
|
|
274
121
|
end
|
|
275
122
|
|
|
276
|
-
def
|
|
277
|
-
|
|
123
|
+
def finished_citation_blocks(event)
|
|
124
|
+
return @stream_citations.keys.sort if event.key?('messageStop') || event.key?('stopReason')
|
|
125
|
+
return [] if %w[delta start contentBlockDelta contentBlockStart].any? { |key| event.key?(key) }
|
|
126
|
+
|
|
127
|
+
[event_block_index(event)].compact
|
|
278
128
|
end
|
|
279
129
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
citation = normalized_delta(event)['citation']
|
|
285
|
-
return nil unless citation.is_a?(Hash)
|
|
130
|
+
def take_block_citations(index)
|
|
131
|
+
length = @stream_text_lengths[index]
|
|
132
|
+
start_index = @stream_text_lengths.sum { |block_index, size| block_index < index ? size : 0 }
|
|
133
|
+
span = length.zero? ? {} : { start_index:, end_index: start_index + length }
|
|
286
134
|
|
|
287
|
-
|
|
135
|
+
Array(@stream_citations.delete(index)).map { |citation| Chat.parse_citation(citation, **span) }
|
|
288
136
|
end
|
|
289
137
|
|
|
290
|
-
def
|
|
138
|
+
def parse_thinking_delta(event)
|
|
291
139
|
reasoning_content = normalized_delta(event)['reasoningContent'] || {}
|
|
292
140
|
|
|
293
141
|
reasoning_text = reasoning_content['reasoningText'] || {}
|
|
@@ -295,33 +143,33 @@ module RubyLLM
|
|
|
295
143
|
return reasoning_content['text'] if reasoning_content.key?('text')
|
|
296
144
|
return '' if [reasoning_text, reasoning_content].any? { |block| block.key?('signature') }
|
|
297
145
|
|
|
298
|
-
|
|
146
|
+
parse_thinking_from_start(event)
|
|
299
147
|
end
|
|
300
148
|
|
|
301
|
-
def
|
|
149
|
+
def parse_thinking_from_start(event)
|
|
302
150
|
start = event.dig('contentBlockStart', 'start', 'reasoningContent') || {}
|
|
303
151
|
return unless start.key?('reasoningText')
|
|
304
152
|
|
|
305
153
|
start.dig('reasoningText', 'text').to_s
|
|
306
154
|
end
|
|
307
155
|
|
|
308
|
-
def
|
|
309
|
-
signature =
|
|
156
|
+
def parse_thinking_signature(event)
|
|
157
|
+
signature = parse_signature_from_delta(event)
|
|
310
158
|
return signature if signature
|
|
311
159
|
|
|
312
|
-
signature =
|
|
160
|
+
signature = parse_signature_from_start(event)
|
|
313
161
|
return signature if signature
|
|
314
162
|
|
|
315
163
|
nil
|
|
316
164
|
end
|
|
317
165
|
|
|
318
|
-
def
|
|
166
|
+
def parse_signature_from_delta(event)
|
|
319
167
|
reasoning_content = normalized_delta(event)['reasoningContent'] || {}
|
|
320
168
|
reasoning_text = reasoning_content['reasoningText'] || {}
|
|
321
169
|
reasoning_text['signature'] || reasoning_content['signature'] || reasoning_content['redactedContent']
|
|
322
170
|
end
|
|
323
171
|
|
|
324
|
-
def
|
|
172
|
+
def parse_signature_from_start(event)
|
|
325
173
|
start = event.dig('contentBlockStart', 'start', 'reasoningContent')
|
|
326
174
|
return nil unless start
|
|
327
175
|
|
|
@@ -333,8 +181,8 @@ module RubyLLM
|
|
|
333
181
|
end
|
|
334
182
|
|
|
335
183
|
def extract_tool_calls(event)
|
|
336
|
-
return
|
|
337
|
-
return
|
|
184
|
+
return parse_tool_call_start(event) if tool_call_start_event?(event)
|
|
185
|
+
return parse_tool_call_delta(event) if tool_call_delta_event?(event)
|
|
338
186
|
|
|
339
187
|
nil
|
|
340
188
|
end
|
|
@@ -347,7 +195,7 @@ module RubyLLM
|
|
|
347
195
|
event['contentBlockDelta'] || event.dig('delta', 'toolUse')
|
|
348
196
|
end
|
|
349
197
|
|
|
350
|
-
def
|
|
198
|
+
def parse_tool_call_start(event)
|
|
351
199
|
tool_use = event.dig('contentBlockStart', 'start', 'toolUse') || event.dig('start', 'toolUse')
|
|
352
200
|
return nil unless tool_use
|
|
353
201
|
|
|
@@ -358,27 +206,27 @@ module RubyLLM
|
|
|
358
206
|
|
|
359
207
|
tool_use_id = tool_use['toolUseId']
|
|
360
208
|
{
|
|
361
|
-
|
|
362
|
-
|
|
209
|
+
event_block_index(event) => ToolCall.new(
|
|
210
|
+
provider_call_id: tool_use_id,
|
|
363
211
|
name: tool_use['name'],
|
|
364
212
|
arguments: tool_use['input'] || {}
|
|
365
213
|
)
|
|
366
214
|
}
|
|
367
215
|
end
|
|
368
216
|
|
|
369
|
-
def
|
|
217
|
+
def parse_tool_call_delta(event)
|
|
370
218
|
return nil if server_tool_block_event?(event)
|
|
371
219
|
|
|
372
220
|
input = normalized_delta(event).dig('toolUse', 'input')
|
|
373
221
|
return nil unless input
|
|
374
222
|
|
|
375
|
-
{
|
|
223
|
+
{ event_block_index(event) => ToolCall.new(name: nil, arguments: input) }
|
|
376
224
|
end
|
|
377
225
|
|
|
378
226
|
# Server-executed tool steps stream as typed toolUse and toolResult
|
|
379
227
|
# block starts. Their input deltas are not function-call arguments,
|
|
380
228
|
# so the block index is remembered and its deltas skipped.
|
|
381
|
-
def
|
|
229
|
+
def parse_server_tool_call_events(event)
|
|
382
230
|
start = event.dig('contentBlockStart', 'start') || event['start']
|
|
383
231
|
return [] unless start.is_a?(Hash)
|
|
384
232
|
|