ruby_llm 1.16.0 → 2.0.0.rc2
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/.rdoc_options +25 -0
- data/README.md +85 -32
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -111
- data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
- data/lib/ruby_llm/active_record/message_methods.rb +113 -136
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -151
- data/lib/ruby_llm/aliases.json +268 -100
- data/lib/ruby_llm/attachment.rb +187 -48
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1127 -198
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +261 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +121 -17
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +159 -23
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +153 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +24 -5
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +55 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +73261 -33733
- data/lib/ruby_llm/models.rb +477 -215
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
- data/lib/ruby_llm/protocols/anthropic.rb +101 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +560 -128
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +5 -14
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
- data/lib/ruby_llm/providers/bedrock.rb +216 -45
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
- data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +6 -18
- data/lib/ruby_llm/providers/mistral/models.rb +57 -23
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +16 -4
- data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +28 -20
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +160 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +3 -2
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -36
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +15 -5
- data/lib/ruby_llm/railtie.rb +7 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +138 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +380 -6
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +339 -97
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -41
- data/lib/ruby_llm/connection.rb +0 -159
- data/lib/ruby_llm/content.rb +0 -91
- data/lib/ruby_llm/deprecator.rb +0 -24
- data/lib/ruby_llm/error_middleware.rb +0 -81
- data/lib/ruby_llm/instrumentation.rb +0 -36
- data/lib/ruby_llm/mime_type.rb +0 -96
- data/lib/ruby_llm/model/info.rb +0 -164
- data/lib/ruby_llm/model_registry.rb +0 -39
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -236
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -218
- data/lib/ruby_llm/streaming.rb +0 -179
- data/lib/ruby_llm/tool_concurrency.rb +0 -105
- data/lib/ruby_llm/utils.rb +0 -130
- data/lib/tasks/models.rake +0 -593
- data/lib/tasks/release.rake +0 -94
- data/lib/tasks/vcr.rake +0 -124
data/lib/ruby_llm/tool.rb
CHANGED
|
@@ -1,175 +1,350 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'schematist'
|
|
4
4
|
|
|
5
5
|
module RubyLLM
|
|
6
|
-
|
|
7
|
-
class Parameter
|
|
6
|
+
class Parameter # :nodoc:
|
|
8
7
|
attr_reader :name, :type, :description, :required
|
|
9
8
|
|
|
10
|
-
def initialize(name, type: 'string',
|
|
9
|
+
def initialize(name, type: 'string', description: nil, required: true)
|
|
11
10
|
@name = name
|
|
12
11
|
@type = type
|
|
13
|
-
@description =
|
|
12
|
+
@description = description
|
|
14
13
|
@required = required
|
|
15
14
|
end
|
|
16
15
|
end
|
|
17
16
|
|
|
18
|
-
#
|
|
17
|
+
# A Tool is an action an AI model can call during a chat. Subclasses
|
|
18
|
+
# describe themselves with ::description, declare their arguments, and
|
|
19
|
+
# implement #execute:
|
|
20
|
+
#
|
|
21
|
+
# class Weather < RubyLLM::Tool
|
|
22
|
+
# description "Gets current weather for a location"
|
|
23
|
+
#
|
|
24
|
+
# def execute(latitude:, longitude:)
|
|
25
|
+
# response = Faraday.get "https://api.open-meteo.com/v1/forecast",
|
|
26
|
+
# latitude: latitude, longitude: longitude,
|
|
27
|
+
# current: "temperature_2m,wind_speed_10m"
|
|
28
|
+
# JSON.parse(response.body)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# chat.with_tools(Weather).ask "What's the weather in Berlin?"
|
|
33
|
+
#
|
|
34
|
+
# When no parameters are declared, the argument schema is inferred from
|
|
35
|
+
# #execute's keyword arguments: required keywords become required string
|
|
36
|
+
# parameters and optional keywords become optional ones. Use ::parameter
|
|
37
|
+
# or ::parameters when arguments need explicit types, descriptions, or
|
|
38
|
+
# structure.
|
|
19
39
|
class Tool
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
KEYWORD_PARAMETER_KINDS = %i[keyreq key].freeze # :nodoc:
|
|
41
|
+
TOOL_CALL_KEYWORD = :tool_call # :nodoc:
|
|
42
|
+
|
|
43
|
+
DUPED_INHERITED_CONFIG = {
|
|
44
|
+
:@declared_parameters => {},
|
|
45
|
+
:@provider_options => {}
|
|
46
|
+
}.freeze
|
|
47
|
+
COPIED_INHERITED_CONFIG = %i[
|
|
48
|
+
@description
|
|
49
|
+
@parameters_schema_definition
|
|
50
|
+
@requires_approval
|
|
51
|
+
@approval_resolver
|
|
52
|
+
].freeze
|
|
53
|
+
private_constant :DUPED_INHERITED_CONFIG, :COPIED_INHERITED_CONFIG
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
55
|
+
class << self
|
|
56
|
+
attr_reader :parameters_schema_definition, :approval_resolver # :nodoc:
|
|
27
57
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
def inherited(subclass) # :nodoc:
|
|
59
|
+
super
|
|
60
|
+
DUPED_INHERITED_CONFIG.each do |ivar, default|
|
|
61
|
+
value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : default
|
|
62
|
+
subclass.instance_variable_set(ivar, value.dup)
|
|
63
|
+
end
|
|
32
64
|
|
|
33
|
-
|
|
65
|
+
COPIED_INHERITED_CONFIG.each do |ivar|
|
|
66
|
+
subclass.instance_variable_set(ivar, instance_variable_get(ivar))
|
|
67
|
+
end
|
|
68
|
+
end
|
|
34
69
|
|
|
35
|
-
|
|
36
|
-
|
|
70
|
+
# Returns the name the model calls this tool by, derived from the class
|
|
71
|
+
# name: underscored, reduced to ASCII, with a trailing "_tool" removed.
|
|
72
|
+
# Override this method to choose a different name.
|
|
73
|
+
#
|
|
74
|
+
# WeatherLookup.tool_name # => "weather_lookup"
|
|
75
|
+
#
|
|
76
|
+
def tool_name
|
|
77
|
+
normalized = name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
|
|
78
|
+
ascii_name = normalized.encode('ASCII', replace: '').gsub(/[^a-zA-Z0-9_-]/, '-')
|
|
79
|
+
Support::Utils.underscore(ascii_name).delete_suffix('_tool')
|
|
80
|
+
end
|
|
37
81
|
|
|
82
|
+
# :call-seq:
|
|
83
|
+
# description(text) -> text
|
|
84
|
+
# description -> string or nil
|
|
85
|
+
#
|
|
86
|
+
# Sets the description the model sees for this tool, or returns the
|
|
87
|
+
# current description when called without an argument.
|
|
88
|
+
#
|
|
89
|
+
# class Weather < RubyLLM::Tool
|
|
90
|
+
# description "Gets current weather for a location"
|
|
91
|
+
# end
|
|
92
|
+
#
|
|
38
93
|
def description(text = nil)
|
|
39
94
|
return @description unless text
|
|
40
95
|
|
|
41
96
|
@description = text
|
|
42
97
|
end
|
|
43
|
-
alias desc description
|
|
44
98
|
|
|
45
|
-
|
|
46
|
-
|
|
99
|
+
# Declares a parameter for the tool. +options+ accepts +type:+
|
|
100
|
+
# (defaults to <tt>'string'</tt>), +description:+, and +required:+
|
|
101
|
+
# (defaults to +true+).
|
|
102
|
+
#
|
|
103
|
+
# class Distance < RubyLLM::Tool
|
|
104
|
+
# description "Calculates distance between two cities"
|
|
105
|
+
# parameter :origin, description: "Origin city name"
|
|
106
|
+
# parameter :destination, description: "Destination city name"
|
|
107
|
+
# parameter :units, type: :string, description: "metric or imperial", required: false
|
|
108
|
+
# end
|
|
109
|
+
#
|
|
110
|
+
def parameter(name, **options)
|
|
111
|
+
declared_parameters[name] = Parameter.new(name, **options)
|
|
47
112
|
end
|
|
48
113
|
|
|
49
|
-
def
|
|
50
|
-
@
|
|
114
|
+
def declared_parameters # :nodoc:
|
|
115
|
+
@declared_parameters ||= {}
|
|
51
116
|
end
|
|
52
117
|
|
|
53
|
-
|
|
54
|
-
|
|
118
|
+
# Sets the JSON Schema for the tool's arguments. Accepts a schema hash,
|
|
119
|
+
# a Schematist::Schema class or instance, or a block written in the
|
|
120
|
+
# schematist DSL. Returns +self+.
|
|
121
|
+
#
|
|
122
|
+
# class Scheduler < RubyLLM::Tool
|
|
123
|
+
# description "Books a meeting"
|
|
124
|
+
#
|
|
125
|
+
# parameters do
|
|
126
|
+
# object :window, description: "Time window to reserve" do
|
|
127
|
+
# string :start, description: "ISO8601 start time"
|
|
128
|
+
# string :finish, description: "ISO8601 end time"
|
|
129
|
+
# end
|
|
130
|
+
# array :participants, of: :string
|
|
131
|
+
# end
|
|
132
|
+
# end
|
|
133
|
+
#
|
|
134
|
+
# Raises ArgumentError when called without a schema or a block.
|
|
135
|
+
def parameters(schema = nil, &block)
|
|
136
|
+
if schema.nil? && block.nil?
|
|
137
|
+
raise ArgumentError, 'parameters requires a schema or a block; declare single arguments with parameter'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
@parameters_schema_definition = SchemaDefinition.new(schema:, block:)
|
|
55
141
|
self
|
|
56
142
|
end
|
|
57
143
|
|
|
58
|
-
|
|
59
|
-
|
|
144
|
+
# Declares that this tool must be approved before it executes. The
|
|
145
|
+
# conversation loop pauses the tool call until a decision is recorded with
|
|
146
|
+
# Chat#approve or Chat#deny, so Chat#complete returns cleanly and
|
|
147
|
+
# can be called again once the decision exists. In Rails the decision
|
|
148
|
+
# persists on the tool call record and survives process restarts.
|
|
149
|
+
#
|
|
150
|
+
# class IssueRefund < RubyLLM::Tool
|
|
151
|
+
# requires_approval
|
|
152
|
+
#
|
|
153
|
+
# def execute(order_id:)
|
|
154
|
+
# Refunds.issue!(order_id)
|
|
155
|
+
# end
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
158
|
+
# Pass a block to resolve the decision yourself instead of using the
|
|
159
|
+
# recorded one. The block receives the ToolCall and returns +true+ to
|
|
160
|
+
# execute, +false+ to deny, or +nil+ while the decision is pending.
|
|
161
|
+
#
|
|
162
|
+
# The block never runs at class definition. The loop consults it
|
|
163
|
+
# whenever it needs the decision, which can be several times while
|
|
164
|
+
# the call is pending, including after a crashed job resumes, so
|
|
165
|
+
# write it as an idempotent read. If it also creates the approval
|
|
166
|
+
# request, make that a find-or-create.
|
|
167
|
+
#
|
|
168
|
+
# requires_approval { |tool_call| Approvals.status(tool_call.id) }
|
|
169
|
+
#
|
|
170
|
+
def requires_approval(&resolver)
|
|
171
|
+
@requires_approval = true
|
|
172
|
+
@approval_resolver = resolver
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def requires_approval? # :nodoc:
|
|
176
|
+
@requires_approval || false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# :call-seq:
|
|
180
|
+
# provider_options(options) -> self
|
|
181
|
+
# provider_options -> hash
|
|
182
|
+
#
|
|
183
|
+
# Sets provider-specific metadata, such as Anthropic's +cache_control+
|
|
184
|
+
# hints, merged verbatim into the tool payload sent to the provider.
|
|
185
|
+
# Without an argument, returns the current options.
|
|
186
|
+
#
|
|
187
|
+
# provider_options cache_control: { type: "ephemeral" }
|
|
188
|
+
#
|
|
189
|
+
# Raises ArgumentError if +options+ is +nil+.
|
|
190
|
+
def provider_options(options = (get = true))
|
|
191
|
+
return @provider_options ||= {} if get
|
|
192
|
+
raise ArgumentError, 'provider_options does not accept nil' if options.nil?
|
|
193
|
+
|
|
194
|
+
@provider_options = options.to_h
|
|
60
195
|
self
|
|
61
196
|
end
|
|
62
197
|
|
|
63
|
-
def
|
|
64
|
-
|
|
198
|
+
def split_result(result) # :nodoc:
|
|
199
|
+
case result
|
|
200
|
+
when Attachment then ['', [result]]
|
|
201
|
+
when Array then split_array_result(result)
|
|
202
|
+
else [result_content(result), []]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def split_array_result(result)
|
|
209
|
+
parts = result.flatten.compact
|
|
210
|
+
return [result_content(result), []] if parts.none?(Attachment)
|
|
211
|
+
|
|
212
|
+
texts, attachments = parts.partition { |part| part.is_a?(String) }
|
|
213
|
+
unless attachments.all?(Attachment)
|
|
214
|
+
raise ArgumentError, 'Tool results mixing attachments can only contain Strings and RubyLLM::Attachments'
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
[texts.join("\n\n"), attachments]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def result_content(result)
|
|
221
|
+
case result
|
|
222
|
+
when String then result
|
|
223
|
+
when Hash, Array, SearchResults then result.to_json
|
|
224
|
+
else result.to_s
|
|
225
|
+
end
|
|
65
226
|
end
|
|
66
227
|
end
|
|
67
228
|
|
|
229
|
+
# Returns the name the model calls this tool by, delegating to
|
|
230
|
+
# ::tool_name. Override either one to choose a different name.
|
|
231
|
+
#
|
|
232
|
+
# WeatherLookup.new.name # => "weather_lookup"
|
|
233
|
+
#
|
|
68
234
|
def name
|
|
69
|
-
|
|
70
|
-
normalized = klass_name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
|
|
71
|
-
normalized.encode('ASCII', replace: '')
|
|
72
|
-
.gsub(/[^a-zA-Z0-9_-]/, '-')
|
|
73
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
74
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
75
|
-
.downcase
|
|
76
|
-
.delete_suffix('_tool')
|
|
235
|
+
self.class.tool_name
|
|
77
236
|
end
|
|
78
237
|
|
|
238
|
+
# Returns the tool description declared on the class with ::description.
|
|
79
239
|
def description
|
|
80
240
|
self.class.description
|
|
81
241
|
end
|
|
82
242
|
|
|
83
|
-
|
|
84
|
-
|
|
243
|
+
# Returns whether this tool was declared with ::requires_approval.
|
|
244
|
+
def requires_approval?
|
|
245
|
+
self.class.requires_approval?
|
|
85
246
|
end
|
|
86
247
|
|
|
87
|
-
def
|
|
88
|
-
self.class.
|
|
248
|
+
def approval_resolver # :nodoc:
|
|
249
|
+
self.class.approval_resolver
|
|
89
250
|
end
|
|
90
251
|
|
|
91
|
-
def
|
|
92
|
-
|
|
252
|
+
def declared_parameters # :nodoc:
|
|
253
|
+
self.class.declared_parameters
|
|
254
|
+
end
|
|
93
255
|
|
|
94
|
-
|
|
95
|
-
|
|
256
|
+
# Returns the provider-specific tool metadata declared on the class.
|
|
257
|
+
def provider_options
|
|
258
|
+
self.class.provider_options
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Returns the JSON Schema for the tool's arguments, whether declared
|
|
262
|
+
# explicitly or inferred from the +execute+ signature.
|
|
263
|
+
def parameters_schema
|
|
264
|
+
return @parameters_schema if defined?(@parameters_schema)
|
|
265
|
+
|
|
266
|
+
@parameters_schema = begin
|
|
267
|
+
definition = self.class.parameters_schema_definition
|
|
96
268
|
if definition&.present?
|
|
97
269
|
definition.json_schema
|
|
98
|
-
elsif
|
|
99
|
-
SchemaDefinition.from_parameters(
|
|
270
|
+
elsif declared_parameters.any?
|
|
271
|
+
SchemaDefinition.from_parameters(declared_parameters)&.json_schema
|
|
100
272
|
else
|
|
101
273
|
SchemaDefinition.from_parameters(inferred_parameters, allow_empty: true)&.json_schema
|
|
102
274
|
end
|
|
103
275
|
end
|
|
104
276
|
end
|
|
105
277
|
|
|
106
|
-
|
|
107
|
-
|
|
278
|
+
# Runs the tool with keyword arguments, validating them against the
|
|
279
|
+
# #execute signature before invoking it. RubyLLM supplies +tool_call:+
|
|
280
|
+
# when the call comes from a chat; pass it yourself only when you need
|
|
281
|
+
# the ToolCall in a direct invocation.
|
|
282
|
+
#
|
|
283
|
+
# Weather.new.call(latitude: 52.52, longitude: 13.405)
|
|
284
|
+
#
|
|
285
|
+
def call(tool_call: nil, **arguments)
|
|
286
|
+
normalized_args = arguments.transform_keys(&:to_sym)
|
|
108
287
|
validation_error = validate_keyword_arguments(normalized_args)
|
|
109
288
|
return { error: "Invalid tool arguments: #{validation_error}" } if validation_error
|
|
110
289
|
|
|
111
290
|
RubyLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
|
|
291
|
+
normalized_args[TOOL_CALL_KEYWORD] = tool_call if execute_accepts_tool_call?
|
|
112
292
|
result = execute(**normalized_args)
|
|
113
293
|
RubyLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
|
|
114
294
|
result
|
|
115
295
|
end
|
|
116
296
|
|
|
297
|
+
# Runs the tool with the arguments chosen by the model. Subclasses must
|
|
298
|
+
# implement this method; the base implementation raises
|
|
299
|
+
# NotImplementedError. The return value is sent back to the model.
|
|
300
|
+
# Return a Hash like <tt>{ error: "..." }</tt> to report a recoverable
|
|
301
|
+
# failure.
|
|
302
|
+
#
|
|
303
|
+
# Declare an optional +tool_call:+ keyword to receive the ToolCall being
|
|
304
|
+
# executed. The keyword is reserved: it never appears in the tool's
|
|
305
|
+
# argument schema and is filled in by RubyLLM, not by the model.
|
|
306
|
+
#
|
|
307
|
+
# def execute(query:, tool_call: nil)
|
|
308
|
+
# AuditLog.create!(tool_call_id: tool_call&.id)
|
|
309
|
+
# Search.run(query)
|
|
310
|
+
# end
|
|
311
|
+
#
|
|
117
312
|
def execute(...)
|
|
118
313
|
raise NotImplementedError, 'Subclasses must implement #execute'
|
|
119
314
|
end
|
|
120
315
|
|
|
121
316
|
protected
|
|
122
317
|
|
|
123
|
-
def
|
|
124
|
-
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def normalize_args(args)
|
|
128
|
-
return {} if args.nil?
|
|
129
|
-
return args.transform_keys(&:to_sym) if args.respond_to?(:transform_keys)
|
|
130
|
-
|
|
131
|
-
{}
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def validate_keyword_arguments(arguments)
|
|
135
|
-
required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments =
|
|
136
|
-
execute_keyword_signature
|
|
137
|
-
|
|
138
|
-
return nil if required_keywords.empty? && optional_keywords.empty? && accepts_positional_arguments
|
|
318
|
+
def validate_keyword_arguments(arguments) # :nodoc:
|
|
319
|
+
required_keywords, optional_keywords, accepts_extra_keywords = execute_keyword_signature
|
|
139
320
|
|
|
140
321
|
argument_keys = arguments.keys
|
|
141
|
-
missing_keyword =
|
|
322
|
+
missing_keyword = (required_keywords - argument_keys).first
|
|
142
323
|
return "missing keyword: #{missing_keyword}" if missing_keyword
|
|
143
324
|
return nil if accepts_extra_keywords
|
|
144
325
|
|
|
145
|
-
|
|
146
|
-
unknown_keyword = first_unknown_keyword(argument_keys, allowed_keywords)
|
|
326
|
+
unknown_keyword = (argument_keys - (required_keywords + optional_keywords)).first
|
|
147
327
|
return "unknown keyword: #{unknown_keyword}" if unknown_keyword
|
|
148
328
|
|
|
149
329
|
nil
|
|
150
330
|
end
|
|
151
331
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
|
|
156
|
-
accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
|
|
157
|
-
accepts_positional_arguments = keyword_signature.any? do |kind, _|
|
|
158
|
-
POSITIONAL_PARAMETER_KINDS.include?(kind)
|
|
332
|
+
def execute_accepts_tool_call? # :nodoc:
|
|
333
|
+
method(:execute).parameters.any? do |kind, name|
|
|
334
|
+
name == TOOL_CALL_KEYWORD && KEYWORD_PARAMETER_KINDS.include?(kind)
|
|
159
335
|
end
|
|
160
|
-
|
|
161
|
-
[required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments]
|
|
162
336
|
end
|
|
163
337
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
338
|
+
def execute_keyword_signature # :nodoc:
|
|
339
|
+
keyword_signature = method(:execute).parameters.reject { |_, name| name == TOOL_CALL_KEYWORD }
|
|
340
|
+
required_keywords = keyword_signature.filter_map { |kind, name| name if kind == :keyreq }
|
|
341
|
+
optional_keywords = keyword_signature.filter_map { |kind, name| name if kind == :key }
|
|
342
|
+
accepts_extra_keywords = keyword_signature.any? { |kind, _| kind == :keyrest }
|
|
167
343
|
|
|
168
|
-
|
|
169
|
-
(argument_keys - allowed_keywords).first
|
|
344
|
+
[required_keywords, optional_keywords, accepts_extra_keywords]
|
|
170
345
|
end
|
|
171
346
|
|
|
172
|
-
def inferred_parameters
|
|
347
|
+
def inferred_parameters # :nodoc:
|
|
173
348
|
required_keywords, optional_keywords, = execute_keyword_signature
|
|
174
349
|
|
|
175
350
|
(required_keywords + optional_keywords).to_h do |name|
|
|
@@ -177,9 +352,7 @@ module RubyLLM
|
|
|
177
352
|
end
|
|
178
353
|
end
|
|
179
354
|
|
|
180
|
-
|
|
181
|
-
# RubyLLM::Schema instances/classes, and DSL blocks.
|
|
182
|
-
class SchemaDefinition
|
|
355
|
+
class SchemaDefinition # :nodoc:
|
|
183
356
|
def self.from_parameters(parameters, allow_empty: false)
|
|
184
357
|
return nil if parameters.nil? || (parameters.empty? && !allow_empty)
|
|
185
358
|
|
|
@@ -233,7 +406,9 @@ module RubyLLM
|
|
|
233
406
|
end
|
|
234
407
|
|
|
235
408
|
def json_schema
|
|
236
|
-
@json_schema ||= RubyLLM::Utils.
|
|
409
|
+
@json_schema ||= RubyLLM::Support::Utils.strip_schema_metadata(
|
|
410
|
+
RubyLLM::Support::Utils.deep_stringify_keys(resolve_schema)
|
|
411
|
+
)
|
|
237
412
|
end
|
|
238
413
|
|
|
239
414
|
private
|
|
@@ -247,7 +422,7 @@ module RubyLLM
|
|
|
247
422
|
|
|
248
423
|
def resolve_direct_schema(schema)
|
|
249
424
|
return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema)
|
|
250
|
-
return RubyLLM::Utils.deep_dup(schema) if schema.is_a?(Hash)
|
|
425
|
+
return RubyLLM::Support::Utils.deep_dup(schema) if schema.is_a?(Hash)
|
|
251
426
|
if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
|
|
252
427
|
return extract_schema(schema.new.to_json_schema)
|
|
253
428
|
end
|
|
@@ -256,7 +431,7 @@ module RubyLLM
|
|
|
256
431
|
end
|
|
257
432
|
|
|
258
433
|
def build_from_block(&)
|
|
259
|
-
schema_class =
|
|
434
|
+
schema_class = Schematist::Schema.create(&)
|
|
260
435
|
extract_schema(schema_class.new.to_json_schema)
|
|
261
436
|
end
|
|
262
437
|
|
|
@@ -264,7 +439,7 @@ module RubyLLM
|
|
|
264
439
|
return nil unless schema_hash.is_a?(Hash)
|
|
265
440
|
|
|
266
441
|
schema = schema_hash[:schema] || schema_hash['schema'] || schema_hash
|
|
267
|
-
RubyLLM::Utils.deep_dup(schema)
|
|
442
|
+
RubyLLM::Support::Utils.deep_dup(schema)
|
|
268
443
|
end
|
|
269
444
|
end
|
|
270
445
|
end
|
data/lib/ruby_llm/tool_call.rb
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
#
|
|
4
|
+
# A ToolCall is a request to run a tool with specific arguments. Instances
|
|
5
|
+
# appear in Message#tool_calls. Local calls are yielded to tool callbacks
|
|
6
|
+
# such as Chat#before_tool_call. Remote calls awaiting approval return
|
|
7
|
+
# +true+ from #remote? and appear in Chat#pending_approvals.
|
|
8
|
+
#
|
|
9
|
+
# chat.before_tool_call do |tool_call|
|
|
10
|
+
# puts "Calling tool: #{tool_call.name}"
|
|
11
|
+
# puts "Arguments: #{tool_call.arguments}"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
5
14
|
class ToolCall
|
|
6
|
-
|
|
15
|
+
include Support::Inspectable
|
|
16
|
+
|
|
17
|
+
# The unique identifier for this call. The tool result message
|
|
18
|
+
# answering this call carries the same id.
|
|
19
|
+
attr_reader :id
|
|
20
|
+
|
|
21
|
+
# The name of the tool the model wants to invoke.
|
|
22
|
+
attr_reader :name
|
|
23
|
+
|
|
24
|
+
# The arguments the model supplied for the invocation, as a Hash.
|
|
25
|
+
attr_reader :arguments
|
|
26
|
+
|
|
27
|
+
# Returns +true+ if the provider executes this call after approval.
|
|
28
|
+
# Local tools executed by the application return +false+.
|
|
29
|
+
def remote? = @remote
|
|
30
|
+
|
|
31
|
+
# The Gemini thought signature attached to this call, or +nil+.
|
|
32
|
+
# RubyLLM replays it to the provider on later requests.
|
|
7
33
|
attr_accessor :thought_signature
|
|
8
34
|
|
|
9
|
-
def initialize(id:, name:, arguments: {}, thought_signature: nil)
|
|
35
|
+
def initialize(id:, name:, arguments: {}, thought_signature: nil, remote: false) # :nodoc:
|
|
10
36
|
@id = id
|
|
11
37
|
@name = name
|
|
12
38
|
@arguments = arguments
|
|
13
39
|
@thought_signature = thought_signature
|
|
40
|
+
@remote = remote
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def inspect_attributes # :nodoc:
|
|
44
|
+
{ id: id, name: name, arguments: arguments, remote: remote? || nil }.compact
|
|
14
45
|
end
|
|
15
46
|
|
|
47
|
+
# Returns the call as a Hash, omitting +nil+ values.
|
|
16
48
|
def to_h
|
|
17
49
|
{
|
|
18
50
|
id: @id,
|
|
19
51
|
name: @name,
|
|
20
52
|
arguments: @arguments,
|
|
53
|
+
remote: remote? || nil,
|
|
21
54
|
thought_signature: @thought_signature
|
|
22
55
|
}.compact
|
|
23
56
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Tools # :nodoc:
|
|
5
|
+
# Normalizes and resolves the server tools a chat enabled with
|
|
6
|
+
# Chat#with_server_tools. Protocols declare an alias table mapping portable
|
|
7
|
+
# names such as +:web_search+ to their wire format; raw Hashes pass through
|
|
8
|
+
# verbatim so new provider tools work without a gem update. :nodoc:
|
|
9
|
+
module ServerTools # :nodoc: all
|
|
10
|
+
# The result of resolving a chat's server tools against a protocol's
|
|
11
|
+
# alias table: tool entries for the payload's tools slot, extra payload
|
|
12
|
+
# fields to merge, and request headers to add.
|
|
13
|
+
class Resolution
|
|
14
|
+
attr_reader :tools, :payload, :headers
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@tools = []
|
|
18
|
+
@payload = {}
|
|
19
|
+
@headers = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add(spec, options)
|
|
23
|
+
entry = spec.is_a?(Proc) ? spec.call(options || {}) : merge_tool_options(spec, options)
|
|
24
|
+
@tools << entry[:tool] if entry[:tool]
|
|
25
|
+
merge_payload(entry[:payload]) if entry[:payload]
|
|
26
|
+
merge_headers(entry[:headers]) if entry[:headers]
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_raw(hash)
|
|
31
|
+
@tools << hash
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def merge_tool_options(spec, options)
|
|
38
|
+
return spec if options.nil? || options.empty?
|
|
39
|
+
|
|
40
|
+
{ tool: Support::Utils.deep_merge(spec.fetch(:tool, spec), Support::Utils.deep_symbolize_keys(options)) }
|
|
41
|
+
.merge(spec.slice(:payload, :headers))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Payload arrays accumulate: every entry contributes its own MCP server.
|
|
45
|
+
def merge_payload(payload)
|
|
46
|
+
@payload = @payload.merge(payload) do |_key, current, addition|
|
|
47
|
+
next current | addition if current.is_a?(Array) && addition.is_a?(Array)
|
|
48
|
+
next Support::Utils.deep_merge(current, addition) if current.is_a?(Hash) && addition.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
addition
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Anthropic-style beta headers combine as comma-separated values.
|
|
55
|
+
def merge_headers(headers)
|
|
56
|
+
headers.each do |key, value|
|
|
57
|
+
@headers[key] = [@headers[key], value].compact.uniq.join(',')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Normalizes with_server_tools arguments into entry hashes:
|
|
65
|
+
# {name:, options:} for aliases and {raw:} for verbatim tool specs.
|
|
66
|
+
def normalize(tools, tools_with_options)
|
|
67
|
+
entries = tools.compact.map { |tool| normalize_positional(tool) }
|
|
68
|
+
entries + tools_with_options.map { |name, options| { name: name.to_sym, options: options.to_h } }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_positional(tool)
|
|
72
|
+
case tool
|
|
73
|
+
when Symbol, String then { name: tool.to_sym, options: {} }
|
|
74
|
+
when Hash then normalized_entry?(tool) ? tool : { raw: tool }
|
|
75
|
+
else
|
|
76
|
+
raise ArgumentError,
|
|
77
|
+
"Server tools must be Symbols or Hashes, got #{tool.class}. " \
|
|
78
|
+
'Pass :web_search for a portable alias or the provider\'s tool definition as a Hash.'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Entries that already went through normalize (an Agent's declared
|
|
83
|
+
# server_tools, for example) pass through unchanged.
|
|
84
|
+
def normalized_entry?(hash)
|
|
85
|
+
hash.key?(:raw) ? hash.size == 1 : hash.size == 2 && hash.key?(:name) && hash.key?(:options)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Resolves normalized entries against a protocol's alias table.
|
|
89
|
+
def resolve(entries, aliases:, owner:)
|
|
90
|
+
entries.each_with_object(Resolution.new) do |entry, resolution|
|
|
91
|
+
if entry[:raw]
|
|
92
|
+
resolution.add_raw(entry[:raw])
|
|
93
|
+
else
|
|
94
|
+
spec = aliases[entry[:name]]
|
|
95
|
+
raise_unknown_alias(entry[:name], aliases, owner) unless spec
|
|
96
|
+
resolution.add(spec, entry[:options])
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def raise_unknown_alias(name, aliases, owner)
|
|
102
|
+
known = aliases.keys.map { |key| ":#{key}" }.join(', ')
|
|
103
|
+
raise UnsupportedServerToolError,
|
|
104
|
+
"#{owner} has no server tool alias :#{name}. Known aliases: #{known}. " \
|
|
105
|
+
'New or unlisted tools work by passing the provider\'s tool definition as a Hash.'
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|