ruby_llm 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +162 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1598 -332
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +67 -28
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +134 -109
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +219 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
data/lib/ruby_llm/mcp/client.rb
CHANGED
|
@@ -6,8 +6,10 @@ module RubyLLM
|
|
|
6
6
|
# 2026-07-28 revision and falls back to the initialize handshake for
|
|
7
7
|
# servers that predate it, declaring only its extensions so those
|
|
8
8
|
# servers never call back. A server that rejects 2026-07-28 while
|
|
9
|
-
# listing it gets the request once more; one that
|
|
10
|
-
#
|
|
9
|
+
# listing it gets the request once more; one that rejects it while
|
|
10
|
+
# listing a revision from LEGACY_VERSIONS, or without listing any,
|
|
11
|
+
# gets the handshake instead; one that answers the handshake with a
|
|
12
|
+
# revision missing from LEGACY_VERSIONS is disconnected.
|
|
11
13
|
#
|
|
12
14
|
# Servers that predate 2026-07-28 may announce that their tools,
|
|
13
15
|
# prompts, or resources changed on the stream of any request. Those
|
|
@@ -18,21 +20,27 @@ module RubyLLM
|
|
|
18
20
|
# never announce the status of tasks. Their subscription counts as
|
|
19
21
|
# acknowledged once a ping confirms the session, starting a new one if
|
|
20
22
|
# it ended.
|
|
21
|
-
|
|
23
|
+
#
|
|
24
|
+
# An MCP builds its own client. Applications use one only to read
|
|
25
|
+
# #meta, when they send a server requests of their own.
|
|
26
|
+
class Client
|
|
27
|
+
# :stopdoc:
|
|
22
28
|
VERSION = '2026-07-28'
|
|
23
29
|
LEGACY_VERSIONS = %w[2025-11-25 2025-06-18 2025-03-26 2024-11-05].freeze
|
|
24
30
|
UNSUPPORTED_VERSION = -32_022
|
|
25
31
|
MODERN_ERRORS = [-32_020, -32_021, UNSUPPORTED_VERSION].freeze
|
|
26
32
|
METHOD_NOT_FOUND = -32_601
|
|
27
33
|
DISCOVERY_TIMEOUT = 10
|
|
34
|
+
PAGE_LIMIT = 1_000
|
|
28
35
|
ACKNOWLEDGED = 'notifications/subscriptions/acknowledged'
|
|
29
36
|
CHANGES = %w[
|
|
30
37
|
notifications/tools/list_changed notifications/prompts/list_changed
|
|
31
38
|
notifications/resources/list_changed notifications/resources/updated
|
|
32
39
|
].freeze
|
|
40
|
+
# :startdoc:
|
|
33
41
|
|
|
34
42
|
# The answer to a request the server sends: RubyLLM only answers pings.
|
|
35
|
-
def self.reply(request)
|
|
43
|
+
def self.reply(request) # :nodoc:
|
|
36
44
|
if request['method'] == 'ping'
|
|
37
45
|
{ jsonrpc: '2.0', id: request['id'], result: {} }
|
|
38
46
|
else
|
|
@@ -40,9 +48,9 @@ module RubyLLM
|
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
|
|
43
|
-
attr_reader :version
|
|
51
|
+
attr_reader :version # :nodoc:
|
|
44
52
|
|
|
45
|
-
def initialize(transport, capabilities: {}, &on_change)
|
|
53
|
+
def initialize(transport, capabilities: {}, &on_change) # :nodoc:
|
|
46
54
|
@transport = transport
|
|
47
55
|
@capabilities = capabilities
|
|
48
56
|
@on_change = on_change
|
|
@@ -50,25 +58,28 @@ module RubyLLM
|
|
|
50
58
|
@subscribed = []
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
def server
|
|
61
|
+
def server # :nodoc:
|
|
54
62
|
@connecting.synchronize { @server ||= discover || handshake }
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
def request(method, params = {}, headers: {}, &)
|
|
65
|
+
def request(method, params = {}, headers: {}, &) # :nodoc:
|
|
58
66
|
server
|
|
59
67
|
call(method, params, headers:, &)
|
|
60
|
-
rescue SessionExpired
|
|
61
|
-
|
|
68
|
+
rescue SessionExpired => e
|
|
69
|
+
reconnect(e.session)
|
|
62
70
|
call(method, params, headers:, &)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
|
-
def list(method, key)
|
|
73
|
+
def list(method, key) # :nodoc:
|
|
66
74
|
page = request(method)
|
|
67
75
|
items = page.fetch(key, [])
|
|
68
|
-
|
|
76
|
+
PAGE_LIMIT.times do
|
|
77
|
+
cursor = page['nextCursor'] or break
|
|
69
78
|
page = request(method, { cursor: })
|
|
70
79
|
items += page.fetch(key, [])
|
|
71
80
|
end
|
|
81
|
+
raise Error, "The server did not finish #{method} in #{PAGE_LIMIT} pages" if page['nextCursor']
|
|
82
|
+
|
|
72
83
|
items
|
|
73
84
|
end
|
|
74
85
|
|
|
@@ -76,18 +87,18 @@ module RubyLLM
|
|
|
76
87
|
# each notification of the subscription, starting with the server's
|
|
77
88
|
# acknowledgment. Returns when the server ends the subscription and
|
|
78
89
|
# raises Error when it refuses one or the stream breaks.
|
|
79
|
-
def listen(changes, &)
|
|
90
|
+
def listen(changes, &) # :nodoc:
|
|
80
91
|
server
|
|
81
92
|
raise ConfigurationError, 'The MCP transport does not respond to listen' unless @transport.respond_to?(:listen)
|
|
82
93
|
|
|
83
94
|
modern? ? subscribe(changes, &) : listen_to_session(changes, &)
|
|
84
95
|
end
|
|
85
96
|
|
|
86
|
-
def modern?
|
|
97
|
+
def modern? # :nodoc:
|
|
87
98
|
version == VERSION
|
|
88
99
|
end
|
|
89
100
|
|
|
90
|
-
def close
|
|
101
|
+
def close # :nodoc:
|
|
91
102
|
@connecting.synchronize do
|
|
92
103
|
@transport.close
|
|
93
104
|
@server = nil
|
|
@@ -96,6 +107,27 @@ module RubyLLM
|
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
110
|
+
# Returns the +_meta+ the client adds to every request it sends a
|
|
111
|
+
# server on the 2026-07-28 revision: the revision, the client's name
|
|
112
|
+
# and version, and its capabilities. Merge it into requests you send
|
|
113
|
+
# the server yourself, so they describe the client the same way:
|
|
114
|
+
#
|
|
115
|
+
# meta = RubyLLM::MCP::Client.new(transport).meta
|
|
116
|
+
# # => { "io.modelcontextprotocol/protocolVersion" => "2026-07-28",
|
|
117
|
+
# # "io.modelcontextprotocol/clientInfo" => { name: "ruby_llm", version: "2.2.0" },
|
|
118
|
+
# # "io.modelcontextprotocol/clientCapabilities" => {} }
|
|
119
|
+
# params = { name: "roots", arguments: {}, _meta: meta }
|
|
120
|
+
#
|
|
121
|
+
# Servers on an older revision get none of these fields: they learn
|
|
122
|
+
# the same from the initialize handshake.
|
|
123
|
+
def meta
|
|
124
|
+
{
|
|
125
|
+
'io.modelcontextprotocol/protocolVersion' => VERSION,
|
|
126
|
+
'io.modelcontextprotocol/clientInfo' => client_info,
|
|
127
|
+
'io.modelcontextprotocol/clientCapabilities' => @capabilities
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
99
131
|
private
|
|
100
132
|
|
|
101
133
|
def discover
|
|
@@ -103,7 +135,7 @@ module RubyLLM
|
|
|
103
135
|
result = call('server/discover', timeout: DISCOVERY_TIMEOUT)
|
|
104
136
|
result if Array(result['supportedVersions']).include?(VERSION)
|
|
105
137
|
rescue Error => e
|
|
106
|
-
raise if MODERN_ERRORS.include?(e.code)
|
|
138
|
+
raise if MODERN_ERRORS.include?(e.code) && !predates_revision?(e)
|
|
107
139
|
end
|
|
108
140
|
|
|
109
141
|
def handshake
|
|
@@ -167,11 +199,15 @@ module RubyLLM
|
|
|
167
199
|
listened = changes.except(:taskIds).transform_keys(&:to_s)
|
|
168
200
|
yield({ 'method' => ACKNOWLEDGED, 'params' => { 'notifications' => listened } })
|
|
169
201
|
@transport.listen(nil, version:, &)
|
|
170
|
-
rescue SessionExpired
|
|
171
|
-
|
|
202
|
+
rescue SessionExpired => e
|
|
203
|
+
reconnect(e.session)
|
|
172
204
|
raise
|
|
173
205
|
end
|
|
174
206
|
|
|
207
|
+
def reconnect(expired)
|
|
208
|
+
@connecting.synchronize { @server = handshake if @server.nil? || @transport.session.equal?(expired) }
|
|
209
|
+
end
|
|
210
|
+
|
|
175
211
|
def cancels?(notification, request)
|
|
176
212
|
notification['method'] == 'notifications/cancelled' && notification.dig('params', 'requestId') == request[:id]
|
|
177
213
|
end
|
|
@@ -182,14 +218,25 @@ module RubyLLM
|
|
|
182
218
|
|
|
183
219
|
def answer(response)
|
|
184
220
|
error = response['error']
|
|
185
|
-
raise Error.new(error['message'], code: error['code'], data: error['data']) if error
|
|
221
|
+
raise Error.new(provider_message: error['message'], code: error['code'], data: error['data']) if error
|
|
186
222
|
|
|
187
223
|
response['result']
|
|
188
224
|
end
|
|
189
225
|
|
|
190
226
|
def offers_version?(error)
|
|
227
|
+
modern? && error.code == UNSUPPORTED_VERSION && supported_versions(error).include?(VERSION)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def predates_revision?(error)
|
|
231
|
+
return false unless error.code == UNSUPPORTED_VERSION
|
|
232
|
+
|
|
233
|
+
supported = supported_versions(error)
|
|
234
|
+
supported.empty? || supported.intersect?(LEGACY_VERSIONS)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def supported_versions(error)
|
|
191
238
|
supported = error.data['supported'] if error.data.is_a?(Hash)
|
|
192
|
-
|
|
239
|
+
Array(supported)
|
|
193
240
|
end
|
|
194
241
|
|
|
195
242
|
def message(method, params = {}, id: nil)
|
|
@@ -197,14 +244,6 @@ module RubyLLM
|
|
|
197
244
|
{ jsonrpc: '2.0', id:, method:, params: }.compact
|
|
198
245
|
end
|
|
199
246
|
|
|
200
|
-
def meta
|
|
201
|
-
{
|
|
202
|
-
'io.modelcontextprotocol/protocolVersion' => VERSION,
|
|
203
|
-
'io.modelcontextprotocol/clientInfo' => client_info,
|
|
204
|
-
'io.modelcontextprotocol/clientCapabilities' => @capabilities
|
|
205
|
-
}
|
|
206
|
-
end
|
|
207
|
-
|
|
208
247
|
def client_info
|
|
209
248
|
{ name: 'ruby_llm', version: RubyLLM::VERSION }
|
|
210
249
|
end
|
|
@@ -14,11 +14,36 @@ module RubyLLM
|
|
|
14
14
|
|
|
15
15
|
def initialize # :nodoc:
|
|
16
16
|
@servers = {}
|
|
17
|
+
@built = {}.compare_by_identity
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
+
def initialize_copy(source) # :nodoc:
|
|
21
|
+
super
|
|
22
|
+
@servers = @servers.dup
|
|
23
|
+
@built = @built.dup
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def connect(server, chat) # :nodoc:
|
|
27
|
+
server = server.new.tap { |built| @built[built] = chat } if server.is_a?(Class)
|
|
28
|
+
replaced = self[server.name]
|
|
29
|
+
check_name(server, replaced) if replaced
|
|
30
|
+
release(replaced, chat) unless replaced.equal?(server)
|
|
20
31
|
@servers[server.name.to_sym] = server
|
|
21
|
-
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def disconnect(chat) # :nodoc:
|
|
35
|
+
each { |server| release(server, chat) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The tools of the servers that can be reached.
|
|
39
|
+
def reachable_tools # :nodoc:
|
|
40
|
+
flat_map { |server| Outages.tools(server) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The names of the servers that can't be reached.
|
|
44
|
+
def unreachable # :nodoc:
|
|
45
|
+
reachable_tools
|
|
46
|
+
select { |server| Outages.down?(server) }.map(&:name)
|
|
22
47
|
end
|
|
23
48
|
|
|
24
49
|
# Returns the server named +name+, or +nil+.
|
|
@@ -42,6 +67,18 @@ module RubyLLM
|
|
|
42
67
|
|
|
43
68
|
private
|
|
44
69
|
|
|
70
|
+
def check_name(server, connected)
|
|
71
|
+
return unless server.class.derived_name? && connected.class.derived_name?
|
|
72
|
+
return if server.class.endpoint == connected.class.endpoint
|
|
73
|
+
|
|
74
|
+
raise ArgumentError, "MCP servers #{connected.class.endpoint} and #{server.class.endpoint} are both named " \
|
|
75
|
+
"#{server.name.inspect}. Pass name: to RubyLLM.mcp to tell them apart"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def release(server, chat)
|
|
79
|
+
server.close if @built[server].equal?(chat) && @built.delete(server)
|
|
80
|
+
end
|
|
81
|
+
|
|
45
82
|
def method_missing(name, *arguments)
|
|
46
83
|
return super unless arguments.empty? && @servers.key?(name)
|
|
47
84
|
|
data/lib/ruby_llm/mcp/content.rb
CHANGED
|
@@ -29,9 +29,14 @@ module RubyLLM
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def attachment(data, mime_type, name)
|
|
32
|
+
file(Base64.decode64(data.to_s), mime_type, name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def file(bytes, mime_type, name)
|
|
36
|
+
name = name.to_s.empty? ? 'resource' : name.to_s
|
|
32
37
|
extension = Marcel::TYPE_EXTS[mime_type]&.first
|
|
33
|
-
name = "#{name}.#{extension}" unless name.
|
|
34
|
-
Attachment.new(StringIO.new(
|
|
38
|
+
name = "#{name}.#{extension}" unless name.include?('.') || extension.nil?
|
|
39
|
+
Attachment.new(StringIO.new(bytes), filename: name)
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def filename(uri)
|
data/lib/ruby_llm/mcp/error.rb
CHANGED
|
@@ -17,10 +17,16 @@ module RubyLLM
|
|
|
17
17
|
# Additional information the server attached to the error, or +nil+.
|
|
18
18
|
attr_reader :data
|
|
19
19
|
|
|
20
|
-
def initialize(message = nil, code: nil, data: nil, response: nil) # :nodoc:
|
|
20
|
+
def initialize(message = nil, code: nil, data: nil, response: nil, error_code: code, **) # :nodoc:
|
|
21
21
|
@code = code
|
|
22
22
|
@data = data
|
|
23
|
-
super(message, response
|
|
23
|
+
super(message, response:, error_code:, **)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def redacted_source
|
|
29
|
+
'The MCP server'
|
|
24
30
|
end
|
|
25
31
|
end
|
|
26
32
|
end
|
data/lib/ruby_llm/mcp/http.rb
CHANGED
|
@@ -4,14 +4,17 @@ module RubyLLM
|
|
|
4
4
|
class MCP
|
|
5
5
|
# Streamable HTTP. Every message is its own POST, answered with a JSON
|
|
6
6
|
# body or with an event stream that carries the request's notifications
|
|
7
|
-
# before its response. Plain HTTP is only allowed on loopback addresses
|
|
8
|
-
# URLs cannot carry credentials, and redirects
|
|
7
|
+
# before its response. Plain HTTP is only allowed on loopback addresses
|
|
8
|
+
# unless +allow_http+ opts in, URLs cannot carry credentials, and redirects
|
|
9
|
+
# are never followed. A cancelled chat closes the stream,
|
|
9
10
|
# which is how 2026-07-28 cancels a request. Servers that predate it may
|
|
10
11
|
# keep a session, which ends with a DELETE when the transport closes.
|
|
11
12
|
#
|
|
12
13
|
# When a stream ends before the response, 2026-07-28 sends the request
|
|
13
|
-
# again with a new ID,
|
|
14
|
-
#
|
|
14
|
+
# again with a new ID, unless it is a tool call that +idempotent+ does
|
|
15
|
+
# not vouch for, since the server may have run it. Older servers resume
|
|
16
|
+
# the stream from its last event ID after the wait they ask for. Either
|
|
17
|
+
# happens a few times at most.
|
|
15
18
|
#
|
|
16
19
|
# A subscription's stream stays open, so it gets a connection of its
|
|
17
20
|
# own: callbacks it runs can make requests of their own, even through
|
|
@@ -30,6 +33,12 @@ module RubyLLM
|
|
|
30
33
|
RECONNECT_DELAY = 1
|
|
31
34
|
CHECK_INTERVAL = 0.1
|
|
32
35
|
|
|
36
|
+
def self.parse(url)
|
|
37
|
+
URI(url.to_s)
|
|
38
|
+
rescue URI::InvalidURIError
|
|
39
|
+
raise ArgumentError, 'The MCP server URL is malformed', cause: nil
|
|
40
|
+
end
|
|
41
|
+
|
|
33
42
|
def self.secure?(url)
|
|
34
43
|
uri = URI(url.to_s)
|
|
35
44
|
return false if uri.userinfo
|
|
@@ -41,15 +50,27 @@ module RubyLLM
|
|
|
41
50
|
LOOPBACK_HOSTS.include?(URI(url.to_s).hostname)
|
|
42
51
|
end
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
# Returns whether +url+ is a web page's address, an +http+ or +https+
|
|
54
|
+
# one. A server's page to visit must be, since anything else, such as
|
|
55
|
+
# a javascript: address, would run in the app that links to it.
|
|
56
|
+
def self.web?(url)
|
|
57
|
+
URI(url.to_s).is_a?(URI::HTTP)
|
|
58
|
+
rescue URI::InvalidURIError
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The session of a server that predates 2026-07-28: the ID the server gave its session, or +nil+.
|
|
63
|
+
attr_reader :session
|
|
64
|
+
|
|
65
|
+
def initialize(url, allow_http: false, headers: {}, timeout: nil, unauthorized: nil, responded: nil,
|
|
66
|
+
idempotent: nil, config: RubyLLM.config)
|
|
67
|
+
@url = self.class.parse(url)
|
|
68
|
+
check_url(allow_http)
|
|
49
69
|
|
|
50
70
|
@headers = headers
|
|
51
71
|
@unauthorized = unauthorized
|
|
52
72
|
@responded = responded
|
|
73
|
+
@idempotent = idempotent
|
|
53
74
|
@timeout = timeout || config.request_timeout
|
|
54
75
|
@connect = lambda do
|
|
55
76
|
Transport::Connection.basic(config) do |faraday|
|
|
@@ -64,7 +85,7 @@ module RubyLLM
|
|
|
64
85
|
stream = post(message, version:, timeout:, params: headers, &)
|
|
65
86
|
RECONNECTS.times do
|
|
66
87
|
if version == Client::VERSION
|
|
67
|
-
break unless stream
|
|
88
|
+
break unless resend?(stream, message)
|
|
68
89
|
|
|
69
90
|
stream = post(message.merge(id: SecureRandom.uuid), version:, timeout:, params: headers, &)
|
|
70
91
|
else
|
|
@@ -90,8 +111,6 @@ module RubyLLM
|
|
|
90
111
|
|
|
91
112
|
stream = post(message, version:, connection: streams, &)
|
|
92
113
|
stream.answer || raise(Error, "#{@url.host} closed the subscription")
|
|
93
|
-
rescue Faraday::Error => e
|
|
94
|
-
raise Error, e.message
|
|
95
114
|
end
|
|
96
115
|
|
|
97
116
|
def close
|
|
@@ -111,6 +130,14 @@ module RubyLLM
|
|
|
111
130
|
|
|
112
131
|
private
|
|
113
132
|
|
|
133
|
+
def check_url(allow_http)
|
|
134
|
+
raise ArgumentError, 'MCP server URLs cannot carry credentials' if @url.userinfo
|
|
135
|
+
return if self.class.secure?(@url) || (allow_http && @url.scheme == 'http')
|
|
136
|
+
|
|
137
|
+
raise ArgumentError, 'MCP servers must use HTTPS, or plain HTTP on a loopback address. ' \
|
|
138
|
+
'For a server on a network you trust, pass allow_http: true with the url'
|
|
139
|
+
end
|
|
140
|
+
|
|
114
141
|
def post(message, version:, timeout: nil, params: {}, connection: @connection, recovered: [], &on_notification)
|
|
115
142
|
session = @session unless message[:method] == 'initialize'
|
|
116
143
|
stream = Stream.new(message[:id], &receiver(version, on_notification))
|
|
@@ -126,11 +153,11 @@ module RubyLLM
|
|
|
126
153
|
remember(message, version, stream.headers)
|
|
127
154
|
stream
|
|
128
155
|
rescue Faraday::Error => e
|
|
129
|
-
raise unless e.response
|
|
156
|
+
raise unanswered(e, stream) unless e.response
|
|
130
157
|
|
|
131
158
|
responded(e.response[:headers])
|
|
132
159
|
return stream if answered?(stream)
|
|
133
|
-
raise SessionExpired
|
|
160
|
+
raise SessionExpired.new("#{@url.host} ended the session", session:) if expired?(e.response, message, session)
|
|
134
161
|
|
|
135
162
|
recovery = recover(e.response, recovered)
|
|
136
163
|
if recovery
|
|
@@ -143,29 +170,30 @@ module RubyLLM
|
|
|
143
170
|
|
|
144
171
|
def resume(stream, version:, timeout:, &on_notification)
|
|
145
172
|
wait(stream.retry_after || RECONNECT_DELAY)
|
|
146
|
-
resumed = Stream.new(stream.id, &receiver(version, on_notification))
|
|
173
|
+
resumed = Stream.new(stream.id, stream, &receiver(version, on_notification))
|
|
147
174
|
get(resumed, version:, timeout:, last_event_id: stream.last_event_id)
|
|
148
175
|
rescue Faraday::Error
|
|
149
176
|
resumed
|
|
150
177
|
end
|
|
151
178
|
|
|
152
179
|
def listen_to_session(version:, &on_notification)
|
|
180
|
+
session = @session
|
|
153
181
|
stream = Stream.new(nil, &receiver(version, on_notification))
|
|
154
|
-
get(stream, version:, connection: streams)
|
|
182
|
+
get(stream, version:, connection: streams, session:)
|
|
155
183
|
nil
|
|
156
184
|
rescue Faraday::Error => e
|
|
157
|
-
raise
|
|
158
|
-
raise SessionExpired
|
|
185
|
+
raise unanswered(e, stream) unless e.response
|
|
186
|
+
raise SessionExpired.new("#{@url.host} ended the session", session:) if e.response[:status] == 404 && session
|
|
159
187
|
raise Error.new("#{@url.host} sends no events", code: Client::METHOD_NOT_FOUND) if e.response[:status] == 405
|
|
160
188
|
|
|
161
189
|
raise failure(e.response, stream)
|
|
162
190
|
end
|
|
163
191
|
|
|
164
|
-
def get(stream, version:, timeout: nil, last_event_id: nil, connection: @connection)
|
|
192
|
+
def get(stream, version:, timeout: nil, last_event_id: nil, connection: @connection, session: @session)
|
|
165
193
|
stream.read do
|
|
166
194
|
connection.get(@url) do |request|
|
|
167
195
|
request.headers.update({ 'Accept' => 'text/event-stream', 'MCP-Protocol-Version' => version,
|
|
168
|
-
'Mcp-Session-Id' =>
|
|
196
|
+
'Mcp-Session-Id' => session, 'Last-Event-ID' => last_event_id }.compact)
|
|
169
197
|
request.headers.update(custom_headers('GET'))
|
|
170
198
|
stream.attach(request, timeout)
|
|
171
199
|
end
|
|
@@ -187,7 +215,7 @@ module RubyLLM
|
|
|
187
215
|
|
|
188
216
|
def answer(request, version:)
|
|
189
217
|
post(Client.reply(request), version:, connection: answers)
|
|
190
|
-
rescue RubyLLM::Error
|
|
218
|
+
rescue RubyLLM::Error => e
|
|
191
219
|
RubyLLM.logger.debug { "#{@url.host} did not take the answer to #{request['method']}: #{e.message}" }
|
|
192
220
|
end
|
|
193
221
|
|
|
@@ -195,6 +223,10 @@ module RubyLLM
|
|
|
195
223
|
@answers ||= @connect.call
|
|
196
224
|
end
|
|
197
225
|
|
|
226
|
+
def resend?(stream, message)
|
|
227
|
+
stream.interrupted? && (message[:method] != 'tools/call' || @idempotent&.call(message.dig(:params, :name)))
|
|
228
|
+
end
|
|
229
|
+
|
|
198
230
|
def resumable?(stream, limit)
|
|
199
231
|
stream.interrupted? && stream.last_event_id && (limit.nil? || (stream.retry_after || 0) <= limit)
|
|
200
232
|
end
|
|
@@ -267,14 +299,32 @@ module RubyLLM
|
|
|
267
299
|
def failure(response, stream)
|
|
268
300
|
case response[:status]
|
|
269
301
|
when 401 then UnauthorizedError.new("#{@url.host} requires authorization", response: response_for(response))
|
|
270
|
-
when 403 then
|
|
302
|
+
when 403 then forbidden(response)
|
|
271
303
|
else
|
|
272
304
|
error = stream.replies.first&.fetch('error', nil) || {}
|
|
273
|
-
|
|
274
|
-
|
|
305
|
+
fallback = "#{@url.host} answered HTTP #{response[:status]}" unless error['message']
|
|
306
|
+
Error.new(fallback, provider_message: error['message'], provider: @url.host,
|
|
307
|
+
code: error['code'], data: error['data'], response: response_for(response))
|
|
275
308
|
end
|
|
276
309
|
end
|
|
277
310
|
|
|
311
|
+
def forbidden(response)
|
|
312
|
+
challenges = OAuth.challenge(response[:headers]&.[]('WWW-Authenticate')).values.select { |c| c[:error] }
|
|
313
|
+
challenge = challenges.find { |c| c[:error] == 'insufficient_scope' } || challenges.first || {}
|
|
314
|
+
details = { response: response_for(response), provider: @url.host, error_code: challenge[:error],
|
|
315
|
+
provider_message: challenge[:error_description] || challenge[:error] }
|
|
316
|
+
message = "#{@url.host} refused the request"
|
|
317
|
+
return ForbiddenError.new(message, **details) unless challenge[:error] == 'insufficient_scope'
|
|
318
|
+
|
|
319
|
+
InsufficientScopeError.new(message, scopes: challenge[:scope].to_s.split, **details)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def unanswered(error, stream)
|
|
323
|
+
return error if stream.raised?(error)
|
|
324
|
+
|
|
325
|
+
ConnectionError.new(error.message, provider: @url.host)
|
|
326
|
+
end
|
|
327
|
+
|
|
278
328
|
def response_for(response)
|
|
279
329
|
Faraday::Response.new(status: response[:status], response_headers: response[:headers],
|
|
280
330
|
body: response[:body])
|
|
@@ -285,15 +335,24 @@ module RubyLLM
|
|
|
285
335
|
# The body is either a single JSON value or a server-sent event
|
|
286
336
|
# stream; the first character tells them apart. An event stream stops
|
|
287
337
|
# being read once it carries the answer to request +id+, since servers
|
|
288
|
-
# may keep it open, and one that breaks midway counts as ended.
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
338
|
+
# may keep it open, and one that breaks midway counts as ended. The
|
|
339
|
+
# stop raises so adapters that pool sockets can discard the connection
|
|
340
|
+
# instead of returning it with unread bytes. Only answers are kept, so
|
|
341
|
+
# a stream that stays open does not grow. A stream that resumes another
|
|
342
|
+
# starts from its last event ID and reconnection time, as an event
|
|
343
|
+
# source keeps both across reconnections. Faraday 2 passes the response
|
|
344
|
+
# headers with each chunk; Faraday 1 has them only once the response ends,
|
|
345
|
+
# so there it reads on, and a stream that stays open ends at the timeout.
|
|
292
346
|
class Stream # :nodoc:
|
|
347
|
+
class Abandoned < StandardError # :nodoc:
|
|
348
|
+
end
|
|
349
|
+
|
|
293
350
|
attr_reader :id, :headers
|
|
294
351
|
|
|
295
|
-
def initialize(id, &on_message)
|
|
352
|
+
def initialize(id, resumed = nil, &on_message)
|
|
296
353
|
@id = id
|
|
354
|
+
@last_event_id = resumed&.last_event_id
|
|
355
|
+
@retry_after = resumed&.retry_after
|
|
297
356
|
@on_message = on_message
|
|
298
357
|
@parser = Transport::EventStreamParser.new
|
|
299
358
|
@body = +''
|
|
@@ -307,13 +366,23 @@ module RubyLLM
|
|
|
307
366
|
end
|
|
308
367
|
|
|
309
368
|
def read(&)
|
|
310
|
-
response =
|
|
369
|
+
response = yield
|
|
311
370
|
@headers = response.headers if @headers.empty? && response.respond_to?(:headers)
|
|
312
371
|
self
|
|
313
|
-
rescue
|
|
314
|
-
|
|
372
|
+
rescue Abandoned
|
|
373
|
+
self
|
|
374
|
+
rescue Faraday::ConnectionFailed => e
|
|
375
|
+
raise if raised?(e) || !events?
|
|
315
376
|
|
|
316
377
|
self
|
|
378
|
+
rescue Faraday::TimeoutError => e
|
|
379
|
+
raise if raised?(e) || !answer
|
|
380
|
+
|
|
381
|
+
self
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def raised?(error)
|
|
385
|
+
@raised.equal?(error)
|
|
317
386
|
end
|
|
318
387
|
|
|
319
388
|
def feed(chunk, _size = nil, env = nil)
|
|
@@ -331,11 +400,11 @@ module RubyLLM
|
|
|
331
400
|
return unless @events
|
|
332
401
|
|
|
333
402
|
@parser.feed(chunk) { |_type, data| receive_event(data) }
|
|
334
|
-
|
|
403
|
+
abandon_if_answered
|
|
335
404
|
end
|
|
336
405
|
|
|
337
406
|
def replies
|
|
338
|
-
receive(@body) if @events == false &&
|
|
407
|
+
receive(@body.slice!(0..)) if @events == false && !@body.empty?
|
|
339
408
|
@replies
|
|
340
409
|
end
|
|
341
410
|
|
|
@@ -348,11 +417,11 @@ module RubyLLM
|
|
|
348
417
|
end
|
|
349
418
|
|
|
350
419
|
def last_event_id
|
|
351
|
-
@parser.last_event_id
|
|
420
|
+
@parser.last_event_id.empty? ? @last_event_id : @parser.last_event_id
|
|
352
421
|
end
|
|
353
422
|
|
|
354
423
|
def retry_after
|
|
355
|
-
@parser.reconnection_time / 1000.0
|
|
424
|
+
@parser.reconnection_time ? @parser.reconnection_time / 1000.0 : @retry_after
|
|
356
425
|
end
|
|
357
426
|
|
|
358
427
|
private
|
|
@@ -361,6 +430,10 @@ module RubyLLM
|
|
|
361
430
|
@events == true
|
|
362
431
|
end
|
|
363
432
|
|
|
433
|
+
def abandon_if_answered
|
|
434
|
+
raise Abandoned if answer && !@headers.empty?
|
|
435
|
+
end
|
|
436
|
+
|
|
364
437
|
def receive_event(data)
|
|
365
438
|
receive(data) unless data.empty?
|
|
366
439
|
end
|
|
@@ -368,11 +441,18 @@ module RubyLLM
|
|
|
368
441
|
def receive(data)
|
|
369
442
|
parsed = JSON.parse(data)
|
|
370
443
|
(parsed.is_a?(Array) ? parsed : [parsed]).grep(Hash).each do |reply|
|
|
371
|
-
reply['method'] ?
|
|
444
|
+
reply['method'] ? deliver(reply) : @replies << reply
|
|
372
445
|
end
|
|
373
446
|
rescue JSON::ParserError
|
|
374
447
|
RubyLLM.logger.debug { 'MCP server sent a message that is not JSON' }
|
|
375
448
|
end
|
|
449
|
+
|
|
450
|
+
def deliver(message)
|
|
451
|
+
@on_message&.call(message)
|
|
452
|
+
rescue StandardError => e
|
|
453
|
+
@raised = e
|
|
454
|
+
raise
|
|
455
|
+
end
|
|
376
456
|
end
|
|
377
457
|
end
|
|
378
458
|
end
|