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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Protocol
|
|
5
|
+
# Prompt cache APIs spell lifetimes as short strings such as "5m" and
|
|
6
|
+
# "1h". A string column stores a Duration as its digits, so digits in a
|
|
7
|
+
# String count as seconds.
|
|
8
|
+
module CacheLifetime # :nodoc:
|
|
9
|
+
UNITS = { 'h' => 3600, 'm' => 60 }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def render(ttl)
|
|
14
|
+
seconds = ttl.is_a?(String) ? Float(ttl, exception: false)&.to_i : ttl.to_i
|
|
15
|
+
return ttl unless seconds
|
|
16
|
+
|
|
17
|
+
unit, size = UNITS.find { |_unit, length| (seconds % length).zero? }
|
|
18
|
+
unit ? "#{seconds / size}#{unit}" : "#{seconds}s"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Protocol
|
|
5
|
+
# Decides which provider-native parts of an assistant message, such as
|
|
6
|
+
# thinking signatures, raw content blocks, and continuations, replay to
|
|
7
|
+
# the model a request goes to.
|
|
8
|
+
module NativeContent # :nodoc:
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# A thinking signature or a provider-shaped content block belongs to the
|
|
12
|
+
# model that produced it, and one provider can serve models that do not
|
|
13
|
+
# read each other's, so a message another model produced replays
|
|
14
|
+
# without them. A message with no known producer replays as it is.
|
|
15
|
+
def foreign_native_content?(message)
|
|
16
|
+
return false unless message.role == :assistant && carries_native_content?(message)
|
|
17
|
+
|
|
18
|
+
producer = producing_entry(message)
|
|
19
|
+
!producer.nil? && (producer.provider != @provider.slug || !current_model?(producer.model))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Usage records the id a chat resolved its model to, and a message built
|
|
23
|
+
# by hand may name the model by an alias of it.
|
|
24
|
+
def current_model?(model_id)
|
|
25
|
+
return true if @model.nil? || model_id == @model.id
|
|
26
|
+
|
|
27
|
+
RubyLLM.models.find(model_id, provider: @provider.slug, config: @config).id == @model.id
|
|
28
|
+
rescue ModelNotFoundError
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Another model of this provider gets the raw content any of its models
|
|
33
|
+
# reads, such as a tool search, and none of the producing model's own.
|
|
34
|
+
def without_foreign_native_content(message)
|
|
35
|
+
portable = portable_raw_content(message) if producer_slug(message) == @provider.slug
|
|
36
|
+
message.without_native_content(raw_content: portable)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def portable_raw_content(_message)
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def carries_native_content?(message)
|
|
44
|
+
return true if message.thinking || message.raw_reasoning || message.raw_content || message.phase
|
|
45
|
+
return true if message.continuation
|
|
46
|
+
|
|
47
|
+
message.tool_call? && message.tool_calls.any?(&:thought_signature)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Only a usage entry names the producer: a model id alone can belong
|
|
51
|
+
# to several providers.
|
|
52
|
+
def producer_slug(message)
|
|
53
|
+
producing_entry(message)&.provider
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def producing_entry(message)
|
|
57
|
+
message.ruby_llm_usage_entries.reverse.find(&:succeeded?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# A thinking signature can be another provider's opaque blob, and a
|
|
61
|
+
# message restored without its usage could come from anyone, so a
|
|
62
|
+
# protocol that verifies signatures gets back only its own.
|
|
63
|
+
def own_signature(message)
|
|
64
|
+
signature = message.thinking&.signature
|
|
65
|
+
signature if signature && producer_slug(message) == @provider.slug
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Protocol
|
|
5
|
+
# Replayed provider content keeps the argument text of calls that were
|
|
6
|
+
# not a JSON object, and some providers reject such a request. These
|
|
7
|
+
# helpers swap that text for empty arguments.
|
|
8
|
+
module ReplayedArguments # :nodoc:
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def malformed_call_ids(message)
|
|
12
|
+
message.tool_calls.filter_map { |call| call.provider_call_id if call.malformed_arguments? }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def blank(entries, message, id:, arguments:, empty:)
|
|
16
|
+
ids = malformed_call_ids(message)
|
|
17
|
+
return entries if ids.empty? || !entries.is_a?(Array)
|
|
18
|
+
|
|
19
|
+
entries.map do |entry|
|
|
20
|
+
next entry unless entry.is_a?(Hash) && entry.key?(arguments)
|
|
21
|
+
next entry unless Array(id).any? { |key| ids.include?(entry[key]) }
|
|
22
|
+
|
|
23
|
+
entry.merge(arguments => empty)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -10,6 +10,7 @@ module RubyLLM
|
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
12
|
@content = +''
|
|
13
|
+
@attachments = []
|
|
13
14
|
@citations = []
|
|
14
15
|
@citation_keys = {}
|
|
15
16
|
@thinking_text = nil
|
|
@@ -20,8 +21,9 @@ module RubyLLM
|
|
|
20
21
|
@raw_content = nil
|
|
21
22
|
@raw_reasoning = nil
|
|
22
23
|
@finish_reason = nil
|
|
23
|
-
@
|
|
24
|
-
@
|
|
24
|
+
@content_filters = {}
|
|
25
|
+
@latest_provider_call_id = nil
|
|
26
|
+
@provider_call_ids_by_index = {}
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def add(chunk)
|
|
@@ -29,12 +31,13 @@ module RubyLLM
|
|
|
29
31
|
@model = chunk.model if @model.to_s.empty?
|
|
30
32
|
|
|
31
33
|
handle_chunk_content(chunk)
|
|
34
|
+
@attachments.concat(chunk.attachments)
|
|
32
35
|
accumulate_citations(chunk.citations)
|
|
33
36
|
append_thinking_from_chunk(chunk)
|
|
34
37
|
accumulate_server_tool_calls(chunk.server_tool_calls)
|
|
35
|
-
|
|
36
|
-
@raw_reasoning = chunk.raw_reasoning if chunk.raw_reasoning
|
|
38
|
+
keep_native_fields(chunk)
|
|
37
39
|
@finish_reason = chunk.finish_reason if chunk.finish_reason
|
|
40
|
+
accumulate_content_filters(chunk.content_filters)
|
|
38
41
|
count_tokens chunk
|
|
39
42
|
RubyLLM.logger.debug { inspect } if RubyLLM.config.log_stream_debug
|
|
40
43
|
end
|
|
@@ -43,6 +46,7 @@ module RubyLLM
|
|
|
43
46
|
Message.new(
|
|
44
47
|
role: :assistant,
|
|
45
48
|
content: content.empty? ? nil : content,
|
|
49
|
+
attachments: @attachments,
|
|
46
50
|
citations: resolved_citations,
|
|
47
51
|
thinking: Thinking.build(
|
|
48
52
|
text: @thinking_text,
|
|
@@ -52,7 +56,10 @@ module RubyLLM
|
|
|
52
56
|
server_tool_calls: @server_tool_calls,
|
|
53
57
|
raw_content: @raw_content,
|
|
54
58
|
raw_reasoning: @raw_reasoning,
|
|
59
|
+
phase: @phase,
|
|
60
|
+
continuation: @continuation,
|
|
55
61
|
finish_reason: @finish_reason,
|
|
62
|
+
content_filters: @content_filters.values,
|
|
56
63
|
model: model,
|
|
57
64
|
tool_calls: tool_calls_from_stream(response),
|
|
58
65
|
raw: response
|
|
@@ -61,6 +68,22 @@ module RubyLLM
|
|
|
61
68
|
|
|
62
69
|
private
|
|
63
70
|
|
|
71
|
+
def keep_native_fields(chunk)
|
|
72
|
+
@raw_content = chunk.raw_content if chunk.raw_content
|
|
73
|
+
@raw_reasoning = chunk.raw_reasoning if chunk.raw_reasoning
|
|
74
|
+
@phase = chunk.phase if chunk.phase
|
|
75
|
+
@continuation = chunk.continuation if chunk.continuation
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A stream can judge the same category again as it goes; a verdict
|
|
79
|
+
# that filtered stays, and otherwise the latest one does.
|
|
80
|
+
def accumulate_content_filters(filters)
|
|
81
|
+
filters.each do |filter|
|
|
82
|
+
key = [filter.source, filter.category]
|
|
83
|
+
@content_filters[key] = filter unless @content_filters[key]&.filtered?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
64
87
|
# Providers like Perplexity repeat the full citation list on every chunk.
|
|
65
88
|
def accumulate_citations(new_citations)
|
|
66
89
|
new_citations.each do |citation|
|
|
@@ -98,53 +121,73 @@ module RubyLLM
|
|
|
98
121
|
|
|
99
122
|
def tool_calls_from_stream(response)
|
|
100
123
|
tool_calls.transform_values do |tc|
|
|
101
|
-
arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
|
|
102
|
-
parse_tool_call_arguments(tc.arguments, response)
|
|
103
|
-
elsif tc.arguments.is_a?(String)
|
|
104
|
-
{}
|
|
105
|
-
else
|
|
106
|
-
tc.arguments
|
|
107
|
-
end
|
|
108
|
-
|
|
109
124
|
ToolCall.new(
|
|
110
125
|
id: tc.id,
|
|
126
|
+
provider_call_id: tc.provider_call_id,
|
|
111
127
|
name: tc.name,
|
|
112
|
-
|
|
128
|
+
namespace: tc.namespace,
|
|
129
|
+
**streamed_arguments(tc, response),
|
|
113
130
|
thought_signature: tc.thought_signature,
|
|
114
131
|
remote: tc.remote?
|
|
115
132
|
)
|
|
116
133
|
end
|
|
117
134
|
end
|
|
118
135
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
def streamed_arguments(tool_call, response)
|
|
137
|
+
arguments = tool_call.arguments
|
|
138
|
+
return { malformed_arguments: tool_call.malformed_arguments } if tool_call.malformed_arguments?
|
|
139
|
+
return { arguments: } unless arguments.is_a?(String)
|
|
140
|
+
raise ToolCallTruncatedError.new(response:) if arguments.empty? && cut_off?(tool_call)
|
|
141
|
+
|
|
142
|
+
ToolCall.parse_arguments(arguments, response:, finish_reason: @finish_reason)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def cut_off?(tool_call)
|
|
146
|
+
@finish_reason == :max_tokens && tool_calls.values.last.equal?(tool_call)
|
|
123
147
|
end
|
|
124
148
|
|
|
125
149
|
def accumulate_tool_calls(new_tool_calls)
|
|
126
150
|
RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
|
|
127
|
-
new_tool_calls.each do |stream_key, tool_call|
|
|
128
|
-
if tool_call
|
|
129
|
-
start_tool_call(stream_key, tool_call)
|
|
130
|
-
else
|
|
151
|
+
new_tool_calls.to_h.each do |stream_key, tool_call|
|
|
152
|
+
if continues_tool_call?(stream_key, tool_call)
|
|
131
153
|
append_tool_call_fragment(stream_key, tool_call)
|
|
154
|
+
else
|
|
155
|
+
if tool_call.provider_call_id.to_s.empty?
|
|
156
|
+
tool_call = ToolCall.new(**tool_call.to_h, provider_call_id: SecureRandom.uuid)
|
|
157
|
+
new_tool_calls[stream_key] = tool_call
|
|
158
|
+
end
|
|
159
|
+
start_tool_call(stream_key, tool_call)
|
|
132
160
|
end
|
|
133
161
|
end
|
|
134
162
|
end
|
|
135
163
|
|
|
164
|
+
def continues_tool_call?(stream_key, tool_call)
|
|
165
|
+
case tool_call.provider_call_id
|
|
166
|
+
when nil then tool_call.name.to_s.empty? || !find_tool_call(stream_key).nil?
|
|
167
|
+
when '' then tool_call.name.to_s.empty?
|
|
168
|
+
else
|
|
169
|
+
!tool_call.arguments.is_a?(Hash) && @provider_call_ids_by_index[stream_key] == tool_call.provider_call_id
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
136
173
|
def start_tool_call(stream_key, tool_call)
|
|
137
|
-
|
|
174
|
+
provider_call_id = tool_call.provider_call_id
|
|
175
|
+
if @tool_calls.key?(provider_call_id) && @provider_call_ids_by_index[stream_key] != provider_call_id
|
|
176
|
+
raise Error, "Duplicate tool call ID in one response: #{provider_call_id.inspect}"
|
|
177
|
+
end
|
|
138
178
|
|
|
139
|
-
@tool_calls[
|
|
140
|
-
id:
|
|
179
|
+
@tool_calls[provider_call_id] = ToolCall.new(
|
|
180
|
+
id: tool_call.id,
|
|
181
|
+
provider_call_id:,
|
|
141
182
|
name: tool_call.name,
|
|
183
|
+
namespace: tool_call.namespace,
|
|
142
184
|
arguments: initial_tool_call_arguments(tool_call),
|
|
185
|
+
malformed_arguments: tool_call.malformed_arguments,
|
|
143
186
|
thought_signature: tool_call.thought_signature,
|
|
144
187
|
remote: tool_call.remote?
|
|
145
188
|
)
|
|
146
|
-
@
|
|
147
|
-
@
|
|
189
|
+
@provider_call_ids_by_index[stream_key] = provider_call_id unless stream_key.nil?
|
|
190
|
+
@latest_provider_call_id = provider_call_id
|
|
148
191
|
end
|
|
149
192
|
|
|
150
193
|
def initial_tool_call_arguments(tool_call)
|
|
@@ -158,18 +201,16 @@ module RubyLLM
|
|
|
158
201
|
existing = find_tool_call(stream_key)
|
|
159
202
|
return unless existing
|
|
160
203
|
|
|
161
|
-
|
|
162
|
-
fragment = '' if fragment.nil?
|
|
163
|
-
existing.arguments << fragment
|
|
204
|
+
existing.arguments << (tool_call.arguments || '') unless existing.arguments.is_a?(Hash)
|
|
164
205
|
return unless tool_call.thought_signature && existing.thought_signature.nil?
|
|
165
206
|
|
|
166
207
|
existing.thought_signature = tool_call.thought_signature
|
|
167
208
|
end
|
|
168
209
|
|
|
169
210
|
def find_tool_call(stream_key)
|
|
170
|
-
return @tool_calls[@
|
|
211
|
+
return @tool_calls[@latest_provider_call_id] if stream_key.nil?
|
|
171
212
|
|
|
172
|
-
@tool_calls[@
|
|
213
|
+
@tool_calls[@provider_call_ids_by_index[stream_key]] || @tool_calls[stream_key]
|
|
173
214
|
end
|
|
174
215
|
|
|
175
216
|
def count_tokens(chunk)
|
|
@@ -179,8 +220,7 @@ module RubyLLM
|
|
|
179
220
|
def handle_chunk_content(chunk)
|
|
180
221
|
accumulate_tool_calls(chunk.tool_calls) if chunk.tool_call?
|
|
181
222
|
|
|
182
|
-
|
|
183
|
-
@content << (content_text.is_a?(String) ? content_text : content_text.to_s)
|
|
223
|
+
@content << chunk.content.to_s
|
|
184
224
|
end
|
|
185
225
|
|
|
186
226
|
def append_thinking_from_chunk(chunk)
|
|
@@ -8,16 +8,48 @@ module RubyLLM
|
|
|
8
8
|
module Streaming # :nodoc: all
|
|
9
9
|
MAX_JSON_BODY_BYTES = 1024 * 1024
|
|
10
10
|
|
|
11
|
-
StreamState = Struct.new(:parser, :buffer, :json_body) do
|
|
11
|
+
StreamState = Struct.new(:parser, :buffer, :json_body, :stray_json, :stray_error) do
|
|
12
12
|
def initialize
|
|
13
13
|
super
|
|
14
14
|
reset
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def reset
|
|
18
|
-
self.parser = Transport::EventStreamParser.new
|
|
18
|
+
self.parser = Transport::EventStreamParser.new(on_unknown_line: method(:keep_stray_line))
|
|
19
19
|
self.buffer = +''
|
|
20
20
|
self.json_body = nil
|
|
21
|
+
self.stray_json = +''
|
|
22
|
+
self.stray_error = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A provider can answer in the middle of a stream with a bare JSON
|
|
26
|
+
# error, which is not an event. Only a line that starts with "{" can
|
|
27
|
+
# open one, so other lines the stream doesn't understand can't hide it.
|
|
28
|
+
# Each object is read as its last line arrives, because more lines can
|
|
29
|
+
# follow it in the same read.
|
|
30
|
+
def keep_stray_line(line)
|
|
31
|
+
return if stray_error
|
|
32
|
+
|
|
33
|
+
self.stray_json = +'' if line.start_with?('{')
|
|
34
|
+
return if stray_json.empty? && !line.start_with?('{')
|
|
35
|
+
if stray_json.bytesize + line.bytesize > MAX_JSON_BODY_BYTES
|
|
36
|
+
raise Error, "Streaming JSON response exceeds #{MAX_JSON_BODY_BYTES} bytes"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
text = line.force_encoding(Encoding::UTF_8).scrub
|
|
40
|
+
stray_json << text << "\n"
|
|
41
|
+
read_stray_json if text.rstrip.end_with?('}')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# A complete object without an error is a line to ignore, and an
|
|
47
|
+
# incomplete one may finish on a later line.
|
|
48
|
+
def read_stray_json
|
|
49
|
+
self.stray_error = stray_json if JSON.parse(stray_json)['error']
|
|
50
|
+
self.stray_json = +''
|
|
51
|
+
rescue JSON::ParserError
|
|
52
|
+
nil
|
|
21
53
|
end
|
|
22
54
|
end
|
|
23
55
|
|
|
@@ -25,42 +57,47 @@ module RubyLLM
|
|
|
25
57
|
|
|
26
58
|
def stream_response(payload, additional_headers = {}, &block)
|
|
27
59
|
accumulator = StreamAccumulator.new
|
|
60
|
+
received = false
|
|
28
61
|
|
|
29
62
|
response = stream_events(stream_url, payload, additional_headers) do |data|
|
|
63
|
+
received = true
|
|
30
64
|
chunk = build_chunk(data)
|
|
31
65
|
accumulator.add chunk
|
|
32
66
|
block.call chunk
|
|
33
67
|
end
|
|
68
|
+
raise Error.new('Provider returned no stream events', response:) unless received
|
|
69
|
+
|
|
70
|
+
if (remainder = stream_remainder)
|
|
71
|
+
accumulator.add remainder
|
|
72
|
+
block.call remainder
|
|
73
|
+
end
|
|
34
74
|
|
|
35
75
|
message = accumulator.to_message(response)
|
|
36
76
|
RubyLLM.logger.debug { "Stream completed: #{message.content}" }
|
|
37
77
|
message
|
|
38
78
|
end
|
|
39
79
|
|
|
80
|
+
# Returns a last Chunk for text a protocol held back while streaming,
|
|
81
|
+
# or +nil+.
|
|
82
|
+
def stream_remainder
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
40
86
|
# Posts +payload+ to +url+ as a server-sent event stream, yielding each
|
|
41
87
|
# parsed event Hash. Returns the Faraday response.
|
|
42
|
-
def stream_events(url, payload, additional_headers = {}, &
|
|
88
|
+
def stream_events(url, payload, additional_headers = {}, &handler)
|
|
43
89
|
progress = {}
|
|
44
90
|
fallback_state = StreamState.new
|
|
45
|
-
on_data = build_on_data_handler(progress, fallback_state: fallback_state) do |
|
|
46
|
-
|
|
91
|
+
on_data = build_on_data_handler(progress, fallback_state: fallback_state) do |event|
|
|
92
|
+
handler.call(event)
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
progress[:callback_error] = e
|
|
95
|
+
raise
|
|
47
96
|
end
|
|
48
97
|
|
|
49
|
-
|
|
98
|
+
post_stream(url, payload, on_data:, progress:, on_reset: fallback_state.method(:reset),
|
|
99
|
+
on_failure: stream_failure(fallback_state)) do |req|
|
|
50
100
|
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
|
51
|
-
(req.options.context ||= {})[Transport::Connection::STREAM_PROGRESS_KEY] = progress
|
|
52
|
-
req.options.context[Transport::ErrorMiddleware::STREAM_RESET_KEY] = fallback_state.method(:reset)
|
|
53
|
-
if faraday_1?
|
|
54
|
-
req.options[:on_data] = on_data
|
|
55
|
-
else
|
|
56
|
-
req.options.on_data = on_data
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def handle_stream(&block)
|
|
62
|
-
build_on_data_handler do |data|
|
|
63
|
-
block.call(build_chunk(data)) if data.is_a?(Hash)
|
|
64
101
|
end
|
|
65
102
|
end
|
|
66
103
|
|
|
@@ -70,6 +107,21 @@ module RubyLLM
|
|
|
70
107
|
Faraday::VERSION.start_with?('1')
|
|
71
108
|
end
|
|
72
109
|
|
|
110
|
+
def post_stream(url, payload, on_data:, progress:, on_reset:, on_failure: nil)
|
|
111
|
+
@connection.post url, payload, usage: @usage_tracker, stream: true do |request|
|
|
112
|
+
yield request if block_given?
|
|
113
|
+
context = (request.options.context ||= {})
|
|
114
|
+
context[Transport::Connection::STREAM_PROGRESS_KEY] = progress
|
|
115
|
+
context[Transport::ErrorMiddleware::STREAM_RESET_KEY] = on_reset
|
|
116
|
+
context[Transport::ErrorMiddleware::STREAM_FAILURE_KEY] = on_failure if on_failure
|
|
117
|
+
request.options.on_data = on_data
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def stream_failure(fallback_state)
|
|
122
|
+
->(env) { raise_stream_failure(env[:streaming_state] || fallback_state, env) }
|
|
123
|
+
end
|
|
124
|
+
|
|
73
125
|
# Some adapters omit the env, so ErrorMiddleware also resets the
|
|
74
126
|
# request's fallback state before each attempt.
|
|
75
127
|
def build_on_data_handler(progress = {}, fallback_state: StreamState.new, &handler)
|
|
@@ -96,10 +148,22 @@ module RubyLLM
|
|
|
96
148
|
if json_body?(chunk, state)
|
|
97
149
|
handle_json_body(chunk, state, env)
|
|
98
150
|
else
|
|
151
|
+
keep_for_failure(chunk, state)
|
|
99
152
|
handle_sse(chunk, state.parser, env, progress, &)
|
|
153
|
+
raise_stray_error(state, env)
|
|
100
154
|
end
|
|
101
155
|
end
|
|
102
156
|
|
|
157
|
+
def raise_stray_error(state, env)
|
|
158
|
+
raise_stream_error(state.stray_error, JSON.parse(state.stray_error), env) if state.stray_error
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Faraday 1 knows the status only once the response ends, so a failure
|
|
162
|
+
# body that is not JSON has to be kept until then.
|
|
163
|
+
def keep_for_failure(chunk, state)
|
|
164
|
+
state.buffer << chunk if faraday_1? && state.buffer.bytesize + chunk.bytesize <= MAX_JSON_BODY_BYTES
|
|
165
|
+
end
|
|
166
|
+
|
|
103
167
|
# Adapters cut reads anywhere, so a read inside an event can start with
|
|
104
168
|
# "{". Only the first bytes of a body can open a bare JSON error.
|
|
105
169
|
def json_body?(chunk, state)
|
|
@@ -107,18 +171,35 @@ module RubyLLM
|
|
|
107
171
|
state.json_body
|
|
108
172
|
end
|
|
109
173
|
|
|
174
|
+
# Faraday 1 knows the status only once the response ends, so the error
|
|
175
|
+
# waits in the buffer until the error middleware reports it.
|
|
110
176
|
def handle_json_body(chunk, state, env)
|
|
111
177
|
if state.buffer.bytesize + chunk.bytesize > MAX_JSON_BODY_BYTES
|
|
112
178
|
raise Error, "Streaming JSON response exceeds #{MAX_JSON_BODY_BYTES} bytes"
|
|
113
179
|
end
|
|
114
180
|
|
|
115
181
|
state.buffer << chunk
|
|
182
|
+
raise_buffered_error(state, env) unless faraday_1?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def raise_buffered_error(state, env)
|
|
116
186
|
parsed = JSON.parse(state.buffer)
|
|
117
187
|
raise_stream_error(state.buffer, parsed, env) if parsed.is_a?(Hash) && parsed['error']
|
|
118
188
|
rescue JSON::ParserError
|
|
119
189
|
RubyLLM.logger.debug { 'Waiting for the remaining JSON response bytes' }
|
|
120
190
|
end
|
|
121
191
|
|
|
192
|
+
# Runs once the response has ended, when a body that never parsed as
|
|
193
|
+
# JSON is the only account of the failure.
|
|
194
|
+
def raise_stream_failure(state, env)
|
|
195
|
+
state.parser.finish
|
|
196
|
+
raise_stray_error(state, env)
|
|
197
|
+
raise_buffered_error(state, env)
|
|
198
|
+
return unless failed_http_status(env) && !state.buffer.empty?
|
|
199
|
+
|
|
200
|
+
env[:streaming_error_response] = build_stream_error_response(state.buffer.dup, env, nil)
|
|
201
|
+
end
|
|
202
|
+
|
|
122
203
|
def handle_failed_response(chunk, buffer, env)
|
|
123
204
|
return if buffer.bytesize + chunk.bytesize > MAX_JSON_BODY_BYTES
|
|
124
205
|
|
|
@@ -142,6 +223,8 @@ module RubyLLM
|
|
|
142
223
|
next if data == '[DONE]'
|
|
143
224
|
|
|
144
225
|
parsed = handle_data(data, env)
|
|
226
|
+
next unless parsed.is_a?(Hash)
|
|
227
|
+
|
|
145
228
|
progress[:started] = true
|
|
146
229
|
yield parsed
|
|
147
230
|
end
|
|
@@ -150,7 +233,7 @@ module RubyLLM
|
|
|
150
233
|
|
|
151
234
|
def handle_data(data, env)
|
|
152
235
|
parsed = JSON.parse(data)
|
|
153
|
-
return parsed unless parsed.is_a?(Hash) && parsed
|
|
236
|
+
return parsed unless parsed.is_a?(Hash) && parsed['error']
|
|
154
237
|
|
|
155
238
|
raise_stream_error(data, parsed, env)
|
|
156
239
|
rescue JSON::ParserError => e
|
|
@@ -188,7 +271,7 @@ module RubyLLM
|
|
|
188
271
|
error_status = failed_http_status(env) || status || 500
|
|
189
272
|
|
|
190
273
|
if faraday_1? || env.nil?
|
|
191
|
-
Struct.new(:body, :status).new(parsed_data, error_status)
|
|
274
|
+
Struct.new(:body, :status, :response_headers).new(parsed_data, error_status, env&.response_headers)
|
|
192
275
|
else
|
|
193
276
|
# Faraday writes body into the request until a status is set.
|
|
194
277
|
env.merge(status: error_status, body: parsed_data)
|
|
@@ -224,7 +307,7 @@ module RubyLLM
|
|
|
224
307
|
# Some adapters do not expose response status during on_data.
|
|
225
308
|
status = env&.status
|
|
226
309
|
|
|
227
|
-
if status && status
|
|
310
|
+
if status && !status.between?(200, 299)
|
|
228
311
|
on_failed_response.call(chunk, env)
|
|
229
312
|
else
|
|
230
313
|
on_chunk.call(chunk, env)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Protocol
|
|
5
|
+
# How a protocol carries Tool.namespace. A namespace the API has no
|
|
6
|
+
# native form for is folded into the function name,
|
|
7
|
+
# <tt>namespace_name</tt>, and the calls that come back under that name
|
|
8
|
+
# are mapped back onto the namespace and the tool's own name.
|
|
9
|
+
module ToolNamespaces
|
|
10
|
+
# Raises for a forced tool choice that names a namespaced tool, for
|
|
11
|
+
# protocols whose tool choice cannot name one.
|
|
12
|
+
def self.validate_tool_choice(tools, choice) # :nodoc:
|
|
13
|
+
return unless tools[choice]&.namespace
|
|
14
|
+
|
|
15
|
+
raise Error, "The namespaced tool #{choice} cannot be forced; use choice: :required instead"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns the error a call answers with when an earlier call of its
|
|
19
|
+
# namespace in the same response failed, or +nil+ when the API runs
|
|
20
|
+
# each call on its own. Protocols whose client toolsets run a
|
|
21
|
+
# response's calls in order and stop at the first failure override
|
|
22
|
+
# this.
|
|
23
|
+
def skipped_tool_call_error(_tool_call)
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def native_tool_namespace?(_namespace) # :nodoc:
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns whether +call+ came back under a namespace the API's own tool
|
|
32
|
+
# search put a deferred function in, rather than one +tools+ declare.
|
|
33
|
+
# Protocols whose tool search does that override this.
|
|
34
|
+
def searched_tool_namespace?(_call, _tools) # :nodoc:
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def complete(messages, tools:, **options, &block) # :nodoc:
|
|
39
|
+
restore_tool_namespaces(super, tools)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render(messages, tools:, **) # :nodoc:
|
|
43
|
+
super(messages.map { |message| flatten_message(message) }, tools: flatten_tools(tools), **)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def count_tokens(messages, tools:, **) # :nodoc:
|
|
47
|
+
super(messages.map { |message| flatten_message(message) }, tools: flatten_tools(tools), **)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def flatten_tools(tools)
|
|
53
|
+
tools.transform_values { |tool| flattened?(tool) ? Tool::Flattened.wrap(tool) : tool }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def restore_tool_namespaces(message, tools)
|
|
57
|
+
return message unless message.is_a?(Message) && message.tool_call?
|
|
58
|
+
|
|
59
|
+
message.with_tool_calls(message.tool_calls.transform_values { |call| restore_tool_namespace(call, tools) })
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def restore_tool_namespace(call, tools)
|
|
63
|
+
if call.namespace.nil?
|
|
64
|
+
tool = tools[call.name.to_sym]
|
|
65
|
+
tool && flattened?(tool) ? restore_tool_call(call, tool) : call
|
|
66
|
+
elsif searched_tool_namespace?(call, tools)
|
|
67
|
+
rebuild_tool_call(call, name: call.name, namespace: nil)
|
|
68
|
+
else
|
|
69
|
+
call
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def flattened?(tool_or_call)
|
|
74
|
+
!tool_or_call.namespace.nil? && !native_tool_namespace?(tool_or_call.namespace)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def flatten_message(message)
|
|
78
|
+
return message unless message.tool_calls.any? { |call| flattened?(call) }
|
|
79
|
+
|
|
80
|
+
message.with_tool_calls(message.tool_calls.transform_values { |call| flatten_tool_call(call) })
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def flatten_tool_call(call)
|
|
84
|
+
return call unless flattened?(call)
|
|
85
|
+
|
|
86
|
+
rebuild_tool_call(call, name: call.qualified_name, namespace: nil)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def restore_tool_call(call, tool)
|
|
90
|
+
rebuild_tool_call(call, name: tool.name, namespace: tool.namespace)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def rebuild_tool_call(call, name:, namespace:)
|
|
94
|
+
ToolCall.new(id: call.id, provider_call_id: call.provider_call_id, name:, namespace:,
|
|
95
|
+
arguments: call.arguments, malformed_arguments: call.malformed_arguments,
|
|
96
|
+
thought_signature: call.thought_signature, remote: call.remote?)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|