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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A Batch is a provider-side batch of requests: chats awaiting a
|
|
5
|
+
# response (or texts awaiting embeddings) go in together, answers come
|
|
6
|
+
# back at batch prices, typically within hours. Persist the id, pick the
|
|
7
|
+
# batch back up from any process, and collect the results once
|
|
8
|
+
# processing ends.
|
|
9
|
+
#
|
|
10
|
+
# chats = documents.map do |doc|
|
|
11
|
+
# RubyLLM.chat(model: "claude-haiku-4-5").ask_later(doc.text)
|
|
12
|
+
# end
|
|
13
|
+
# batch = RubyLLM.batch(chats)
|
|
14
|
+
# batch.id # => "msgbatch_01EhcDuvb5XfWqcdJArbsfNX"
|
|
15
|
+
# batch.refresh.complete? # => false, check back later
|
|
16
|
+
# batch.messages # the responses, in submission order
|
|
17
|
+
#
|
|
18
|
+
class Batch
|
|
19
|
+
include Support::Inspectable
|
|
20
|
+
|
|
21
|
+
AWAITING_ROLES = %i[user tool].freeze # :nodoc:
|
|
22
|
+
|
|
23
|
+
# The provider's batch id. Persist it to load the batch again later
|
|
24
|
+
# from any process with ::find.
|
|
25
|
+
attr_reader :id
|
|
26
|
+
|
|
27
|
+
# The provider-neutral lifecycle status: +:pending+, +:succeeded+,
|
|
28
|
+
# +:failed+, or +:cancelled+. Refreshed by #refresh.
|
|
29
|
+
attr_reader :status
|
|
30
|
+
|
|
31
|
+
# The provider-reported status string, such as "in_progress".
|
|
32
|
+
# Refreshed by #refresh.
|
|
33
|
+
attr_reader :raw_status
|
|
34
|
+
|
|
35
|
+
# The provider-reported request tallies by state, or +nil+ when the
|
|
36
|
+
# provider does not report them.
|
|
37
|
+
attr_reader :request_counts
|
|
38
|
+
|
|
39
|
+
# The submitted Chat objects in order, or +nil+ when the batch was
|
|
40
|
+
# loaded by id via ::find or holds embedding requests.
|
|
41
|
+
attr_reader :chats
|
|
42
|
+
|
|
43
|
+
# The submitted EmbeddingRequest objects in order, or +nil+ when the
|
|
44
|
+
# batch was loaded by id via ::find or holds chats.
|
|
45
|
+
attr_reader :requests
|
|
46
|
+
|
|
47
|
+
# The normalized outcome of each collected request, in submission order.
|
|
48
|
+
# Values are +:succeeded+, +:failed+, or +:cancelled+.
|
|
49
|
+
attr_reader :statuses
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
# Submits chats or embedding requests to their shared provider as a
|
|
53
|
+
# batch and returns a new Batch. Accepts a single Chat or an array.
|
|
54
|
+
# Every chat must be awaiting the model (see Chat#ask_later), and all
|
|
55
|
+
# requests must use the same provider.
|
|
56
|
+
#
|
|
57
|
+
# chats = tickets.map do |ticket|
|
|
58
|
+
# RubyLLM.chat(model: "claude-haiku-4-5").ask_later(ticket.body)
|
|
59
|
+
# end
|
|
60
|
+
# batch = RubyLLM::Batch.submit(chats)
|
|
61
|
+
# batch.status # => :pending
|
|
62
|
+
# batch.raw_status # => "in_progress"
|
|
63
|
+
#
|
|
64
|
+
# Raises ArgumentError if the batch is empty, mixes providers, mixes
|
|
65
|
+
# chats with embedding requests, or includes a chat that is not
|
|
66
|
+
# awaiting the model.
|
|
67
|
+
def submit(chats)
|
|
68
|
+
records = wrap_records(chats)
|
|
69
|
+
return submit_embeddings(records) if records.any?(EmbeddingRequest)
|
|
70
|
+
|
|
71
|
+
submit_chats(records)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns a Batch reflecting the provider's current state for +id+.
|
|
75
|
+
# Use it to pick a batch back up from any process.
|
|
76
|
+
#
|
|
77
|
+
# batch = RubyLLM::Batch.find("msgbatch_01EhcDuvb5XfWqcdJArbsfNX",
|
|
78
|
+
# provider: :anthropic)
|
|
79
|
+
# batch.complete? # => true
|
|
80
|
+
#
|
|
81
|
+
# Pass +context:+ to use a Context in place of the global
|
|
82
|
+
# configuration. Raises ArgumentError if +provider+ is not given.
|
|
83
|
+
def find(id, provider: nil, context: nil)
|
|
84
|
+
config = context&.config || RubyLLM.config
|
|
85
|
+
persisted = config.batch_store&.fetch(id, provider:, context:)
|
|
86
|
+
return persisted if persisted
|
|
87
|
+
|
|
88
|
+
unless provider
|
|
89
|
+
raise ArgumentError, 'Provider must be specified to find a batch that is not persisted by RubyLLM'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
provider = Provider.resolve!(provider).new(config)
|
|
93
|
+
raise Error, "#{provider.slug} doesn't support batch requests" unless provider.batches?
|
|
94
|
+
|
|
95
|
+
new(provider:, store: config.batch_store, **provider.find_batch(id))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def submit_chats(records)
|
|
101
|
+
chats = normalize_chats(records)
|
|
102
|
+
|
|
103
|
+
provider = shared_provider(chats)
|
|
104
|
+
payload = { provider: provider.slug, provider_class: provider.class.display_name, requests: chats.size }
|
|
105
|
+
RubyLLM.instrument('batch.ruby_llm', payload, config: provider.config) do |event|
|
|
106
|
+
requests = chats.each_with_index.map do |chat, index|
|
|
107
|
+
{ custom_id: index.to_s, model: chat.model.id, payload: chat.render }
|
|
108
|
+
end
|
|
109
|
+
store = provider.config.batch_store
|
|
110
|
+
batch = new(provider:, chats:, store:, **provider.create_batch(requests))
|
|
111
|
+
store&.persist(batch, records)
|
|
112
|
+
event[:batch_id] = batch.id
|
|
113
|
+
batch
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def submit_embeddings(requests)
|
|
118
|
+
unless requests.all?(EmbeddingRequest)
|
|
119
|
+
raise ArgumentError, 'A batch takes chats or embedding requests, not both'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
provider = shared_provider(requests)
|
|
123
|
+
payload = { provider: provider.slug, provider_class: provider.class.display_name, requests: requests.size }
|
|
124
|
+
RubyLLM.instrument('batch.ruby_llm', payload, config: provider.config) do |event|
|
|
125
|
+
lines = requests.each_with_index.map do |request, index|
|
|
126
|
+
{ custom_id: index.to_s, model: request.model.id, payload: request.render, text: request.text }
|
|
127
|
+
end
|
|
128
|
+
batch = new(provider:, requests:, store: provider.config.batch_store, **provider.create_batch(lines))
|
|
129
|
+
event[:batch_id] = batch.id
|
|
130
|
+
batch
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def wrap_records(records)
|
|
135
|
+
return [records] if records.respond_to?(:to_llm)
|
|
136
|
+
|
|
137
|
+
case records
|
|
138
|
+
when Chat, EmbeddingRequest then [records]
|
|
139
|
+
else Array(records)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def normalize_chats(records)
|
|
144
|
+
normalized = records.map { |chat| chat.respond_to?(:to_llm) ? chat.to_llm : chat }
|
|
145
|
+
raise ArgumentError, 'Cannot submit an empty batch' if normalized.empty?
|
|
146
|
+
|
|
147
|
+
unless normalized.all? { |chat| awaiting_model?(chat) }
|
|
148
|
+
raise ArgumentError,
|
|
149
|
+
'Every chat in a batch must be awaiting the model; stage one with ask_later, or run_tools first'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
normalized
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def awaiting_model?(chat)
|
|
156
|
+
!chat.complete? && AWAITING_ROLES.include?(chat.messages.last&.role)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def shared_provider(chats)
|
|
160
|
+
slugs = chats.map { |chat| chat.provider.slug }.uniq
|
|
161
|
+
raise ArgumentError, "A batch takes one provider per submission, got: #{slugs.join(', ')}" if slugs.size > 1
|
|
162
|
+
|
|
163
|
+
provider = chats.first.provider
|
|
164
|
+
raise Error, "#{provider.slug} doesn't support batch requests" unless provider.batches?
|
|
165
|
+
|
|
166
|
+
provider
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def initialize(provider:, chats: nil, requests: nil, batch_protocol: nil, store: nil, **attributes) # :nodoc:
|
|
171
|
+
@provider = provider
|
|
172
|
+
@chats = chats
|
|
173
|
+
@requests = requests
|
|
174
|
+
@batch_protocol = protocol_name(batch_protocol)
|
|
175
|
+
@store = store
|
|
176
|
+
@delivered = {}
|
|
177
|
+
@statuses = []
|
|
178
|
+
apply(attributes)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# The slug of the provider running the batch, as a String.
|
|
182
|
+
def provider
|
|
183
|
+
@provider.slug
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
attr_reader :batch_protocol, :reported_cost # :nodoc:
|
|
187
|
+
|
|
188
|
+
# Returns whether the batch has finished processing, as of the last
|
|
189
|
+
# state fetched from the provider. Never contacts the provider; poll
|
|
190
|
+
# with #refresh.
|
|
191
|
+
#
|
|
192
|
+
# sleep 60 until batch.refresh.complete?
|
|
193
|
+
#
|
|
194
|
+
def complete?
|
|
195
|
+
@completed
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Returns whether the provider completed the batch successfully.
|
|
199
|
+
def succeeded?
|
|
200
|
+
status == :succeeded
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Returns whether the provider failed or expired the batch.
|
|
204
|
+
def failed?
|
|
205
|
+
status == :failed
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Returns whether the provider cancelled the batch.
|
|
209
|
+
def cancelled?
|
|
210
|
+
status == :cancelled
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Re-fetches the batch from the provider, updating #status, #raw_status,
|
|
214
|
+
# #request_counts, and #complete?. Returns +self+.
|
|
215
|
+
def refresh
|
|
216
|
+
apply(@provider.find_batch(id))
|
|
217
|
+
persist_state
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Asks the provider to cancel the batch and applies the new state.
|
|
222
|
+
# Requests already processed still return results. Returns +self+.
|
|
223
|
+
def cancel
|
|
224
|
+
apply(@provider.cancel_batch(id))
|
|
225
|
+
persist_state
|
|
226
|
+
self
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Returns the answers in submission order, +nil+ where a request
|
|
230
|
+
# failed. In a chat batch the answers are Messages, each also appended
|
|
231
|
+
# to its chat; in an embeddings batch they are Embeddings, each also
|
|
232
|
+
# hydrated into its request's EmbeddingRequest#result. Fetches results
|
|
233
|
+
# from the provider; cached once #complete? is true, so collecting
|
|
234
|
+
# early keeps reading fresh.
|
|
235
|
+
#
|
|
236
|
+
# batch.messages.each do |message|
|
|
237
|
+
# puts message.content
|
|
238
|
+
# end
|
|
239
|
+
#
|
|
240
|
+
def messages
|
|
241
|
+
return @messages if @messages
|
|
242
|
+
|
|
243
|
+
collected = collect_results
|
|
244
|
+
@messages = collected if @completed
|
|
245
|
+
collected
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
alias results messages
|
|
249
|
+
|
|
250
|
+
# Returns token usage aggregated across the batch's collected responses.
|
|
251
|
+
def tokens
|
|
252
|
+
Tokens.aggregate(messages.compact.map(&:tokens))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Returns a Cost for the batch. Uses the provider's reported total when
|
|
256
|
+
# available, otherwise aggregates collected response costs at batch rates.
|
|
257
|
+
# The total is +nil+ until the batch ends or when pricing is unknown.
|
|
258
|
+
def cost
|
|
259
|
+
return Cost.aggregate([reported_cost], complete: complete?) if reported_cost
|
|
260
|
+
return Cost.aggregate([], complete: false) unless complete?
|
|
261
|
+
|
|
262
|
+
Cost.aggregate(messages.compact.map(&:cost))
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
def persist_state
|
|
268
|
+
@store&.sync(self)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def apply(attributes)
|
|
272
|
+
@batch_protocol = protocol_name(attributes[:batch_protocol]) if attributes[:batch_protocol]
|
|
273
|
+
@id = attributes.fetch(:id)
|
|
274
|
+
@raw_status = attributes.fetch(:raw_status)
|
|
275
|
+
@completed = attributes.fetch(:completed)
|
|
276
|
+
@request_counts = attributes[:request_counts]
|
|
277
|
+
@request_count = attributes[:request_count]
|
|
278
|
+
@reported_cost = attributes[:reported_cost] if attributes[:reported_cost]
|
|
279
|
+
@status = @provider.batch_status(@raw_status, completed: @completed, batch_protocol: @batch_protocol)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def protocol_name(protocol)
|
|
283
|
+
return if protocol.nil?
|
|
284
|
+
|
|
285
|
+
protocol.is_a?(Module) ? @provider.batch_protocol_name(protocol) : protocol.to_s
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def collect_results
|
|
289
|
+
results = @provider.batch_results(id, batch_protocol: @batch_protocol)
|
|
290
|
+
slots = Array.new(result_slot_count(results))
|
|
291
|
+
|
|
292
|
+
results.each do |index, result, failure_status|
|
|
293
|
+
slots[index] = result
|
|
294
|
+
deliver(index, result, failure_status)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
fill_missing_statuses(slots.size) if complete?
|
|
298
|
+
|
|
299
|
+
slots
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def result_slot_count(results)
|
|
303
|
+
chats&.size || requests&.size || @request_count || ((results.map(&:first).max || -1) + 1)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def fill_missing_statuses(size)
|
|
307
|
+
missing_status = cancelled? ? :cancelled : :failed
|
|
308
|
+
size.times { |index| statuses[index] ||= missing_status }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Collecting early keeps reading fresh, so a result already delivered
|
|
312
|
+
# comes back on every later poll: hand each one over once.
|
|
313
|
+
def deliver(index, result, failure_status)
|
|
314
|
+
statuses[index] = result ? :succeeded : failure_status
|
|
315
|
+
return unless result
|
|
316
|
+
|
|
317
|
+
if result.is_a?(Embedding)
|
|
318
|
+
deliver_embedding(index, result)
|
|
319
|
+
else
|
|
320
|
+
deliver_message(index, result)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def deliver_embedding(index, embedding)
|
|
325
|
+
request = requests&.[](index)
|
|
326
|
+
delivered = @delivered[index]
|
|
327
|
+
attach_batch_usage(
|
|
328
|
+
embedding,
|
|
329
|
+
operation: :embedding,
|
|
330
|
+
model: request&.model,
|
|
331
|
+
category: :embeddings,
|
|
332
|
+
instrument: !request.nil? && !delivered
|
|
333
|
+
)
|
|
334
|
+
return if delivered
|
|
335
|
+
|
|
336
|
+
@delivered[index] = true
|
|
337
|
+
request&.result = embedding
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def deliver_message(index, message)
|
|
341
|
+
chat = chats&.[](index)
|
|
342
|
+
delivered = @delivered[index] || (chat && already_in_chat?(chat, message))
|
|
343
|
+
attach_batch_usage(
|
|
344
|
+
message,
|
|
345
|
+
operation: :chat,
|
|
346
|
+
model: chat&.model,
|
|
347
|
+
category: :text_tokens,
|
|
348
|
+
instrument: !chat.nil? && !delivered
|
|
349
|
+
)
|
|
350
|
+
return if delivered
|
|
351
|
+
|
|
352
|
+
@delivered[index] = true
|
|
353
|
+
chat&.add_completion(message, record_usage: true)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def attach_batch_usage(result, operation:, model:, category:, instrument:)
|
|
357
|
+
return unless result.ruby_llm_usage_entries.empty?
|
|
358
|
+
|
|
359
|
+
model ||= RubyLLM.models.find(result.model, provider: @provider.slug, config: @provider.config)
|
|
360
|
+
entry = Accounting::Usage::Entry.new(
|
|
361
|
+
operation:,
|
|
362
|
+
provider: @provider.slug,
|
|
363
|
+
model: result.model || model.id,
|
|
364
|
+
status: :succeeded,
|
|
365
|
+
tokens: result.tokens,
|
|
366
|
+
cost: @provider.batch_cost(result.tokens, model:, category:),
|
|
367
|
+
message: result.is_a?(Message) ? result : nil
|
|
368
|
+
)
|
|
369
|
+
result.ruby_llm_usage_entries = [entry]
|
|
370
|
+
Accounting::Usage.instrument(entry, config: @provider.config) if instrument
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# A plain answer is the chat's last message once it arrives. A tool-call
|
|
374
|
+
# answer is not: running its tools adds messages after it, so we match on its
|
|
375
|
+
# tool-call ids instead.
|
|
376
|
+
def already_in_chat?(chat, message)
|
|
377
|
+
if message.tool_call?
|
|
378
|
+
chat.messages.any? { |m| m.tool_call? && m.tool_calls.keys.intersect?(message.tool_calls.keys) }
|
|
379
|
+
else
|
|
380
|
+
!AWAITING_ROLES.include?(chat.messages.last&.role)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
module Helpers # :nodoc:
|
|
385
|
+
private
|
|
386
|
+
|
|
387
|
+
def batch_result_index(id)
|
|
388
|
+
Integer(id)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def batch_failure(custom_id, detail, status: 'failed')
|
|
392
|
+
RubyLLM.logger.warn ["Batch request #{custom_id} #{status}", detail].compact.join(': ')
|
|
393
|
+
status.to_s.match?(/cancel/i) ? :cancelled : :failed
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def batch_error_message(line)
|
|
397
|
+
response = line['response']
|
|
398
|
+
body = response['body'] if response.is_a?(Hash)
|
|
399
|
+
body_error = body['error'] if body.is_a?(Hash)
|
|
400
|
+
response_error = response['error'] if response.is_a?(Hash)
|
|
401
|
+
|
|
402
|
+
batch_error_value(line['error']) ||
|
|
403
|
+
line['error_message'] ||
|
|
404
|
+
batch_error_value(body_error) ||
|
|
405
|
+
batch_error_value(response_error)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def batch_error_value(error)
|
|
409
|
+
case error
|
|
410
|
+
when Hash then error['message']
|
|
411
|
+
when String then error
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def single_batch_model!(requests, provider_name)
|
|
416
|
+
models = requests.map { |request| request.fetch(:model) }.uniq
|
|
417
|
+
return models.first if models.one?
|
|
418
|
+
|
|
419
|
+
raise Error, "#{provider_name} batch requests must use one model per submission"
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def batch_payload(request, except: [])
|
|
423
|
+
excluded = (Array(except) + [:stream]).map(&:to_s)
|
|
424
|
+
request.fetch(:payload).reject { |key, _| excluded.include?(key.to_s) }
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def inspect_attributes # :nodoc:
|
|
429
|
+
{ id: id, status: status, raw_status: raw_status, chats: chats&.count, requests: requests&.count }
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A CachedContent is a provider-side prompt cache resource. Create one
|
|
5
|
+
# with ::create from a long, stable prompt prefix, then attach it to a
|
|
6
|
+
# chat so later requests read the cached tokens instead of resending
|
|
7
|
+
# them. This lifecycle requires a provider with managed cache resources;
|
|
8
|
+
# automatic prompt caching is configured with Chat#with_caching.
|
|
9
|
+
#
|
|
10
|
+
# cache = RubyLLM.cache(big_document, model: 'gemini-3.7-flash', ttl: 3600)
|
|
11
|
+
# chat = RubyLLM.chat(model: 'gemini-3.7-flash').with_caching(id: cache)
|
|
12
|
+
# chat.ask "What does the document conclude?"
|
|
13
|
+
# cache.delete
|
|
14
|
+
#
|
|
15
|
+
# Cache names are provider-owned. Persist #provider alongside #name and
|
|
16
|
+
# pass it back when finding the cache later.
|
|
17
|
+
class CachedContent
|
|
18
|
+
include Support::Inspectable
|
|
19
|
+
|
|
20
|
+
def inspect_attributes # :nodoc:
|
|
21
|
+
{ name: name, model: model, provider: provider, expires_at: expires_at }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The provider-assigned resource name, such as
|
|
25
|
+
# <tt>"cachedContents/abc123"</tt>.
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
# The model the cache was created for.
|
|
29
|
+
attr_reader :model
|
|
30
|
+
|
|
31
|
+
# The slug of the provider that stores the cache.
|
|
32
|
+
attr_reader :provider
|
|
33
|
+
|
|
34
|
+
# The Time the provider stored the cache.
|
|
35
|
+
attr_reader :created_at
|
|
36
|
+
|
|
37
|
+
# The Time the provider will delete the cache.
|
|
38
|
+
attr_reader :expires_at
|
|
39
|
+
|
|
40
|
+
# The number of tokens stored in the cache.
|
|
41
|
+
attr_reader :tokens
|
|
42
|
+
|
|
43
|
+
# The raw provider response data for the cache, as a Hash.
|
|
44
|
+
attr_reader :metadata
|
|
45
|
+
|
|
46
|
+
def initialize(name:, **attributes) # :nodoc:
|
|
47
|
+
@name = name
|
|
48
|
+
@model = attributes[:model]
|
|
49
|
+
@provider_instance = attributes[:provider_instance]
|
|
50
|
+
@provider = attributes[:provider] || @provider_instance&.slug
|
|
51
|
+
@created_at = attributes[:created_at]
|
|
52
|
+
@expires_at = attributes[:expires_at]
|
|
53
|
+
@tokens = attributes[:tokens]
|
|
54
|
+
@metadata = attributes[:metadata] || {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Deletes the cache resource from the provider. Returns +self+.
|
|
58
|
+
def delete
|
|
59
|
+
@provider_instance.delete_cache(name)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Extends the cache's lifetime to +ttl:+ seconds from now, given as an
|
|
64
|
+
# Integer or a provider duration string such as <tt>"600s"</tt>.
|
|
65
|
+
# Updates #expires_at and returns +self+.
|
|
66
|
+
#
|
|
67
|
+
# cache.renew(ttl: 3600)
|
|
68
|
+
#
|
|
69
|
+
def renew(ttl:)
|
|
70
|
+
refreshed = @provider_instance.extend_cache(name, ttl: ttl)
|
|
71
|
+
@expires_at = refreshed.expires_at
|
|
72
|
+
@metadata = refreshed.metadata
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Creates a provider-side prompt cache from +content+ and returns a
|
|
77
|
+
# CachedContent. Also available as RubyLLM.cache.
|
|
78
|
+
#
|
|
79
|
+
# RubyLLM::CachedContent.create(big_document, model: 'gemini-3.7-flash')
|
|
80
|
+
#
|
|
81
|
+
# +content+ is the text to cache; pass file attachments with +with:+
|
|
82
|
+
# the way Chat#ask accepts them. +instructions:+ caches a system
|
|
83
|
+
# prompt alongside the content, and +ttl:+ sets the cache lifetime in
|
|
84
|
+
# seconds (Integer) or as a provider duration string such as
|
|
85
|
+
# <tt>"300s"</tt>. When +provider:+ is omitted, the model's default
|
|
86
|
+
# provider is used. The content must exceed the model's minimum
|
|
87
|
+
# cacheable token count.
|
|
88
|
+
def self.create(content, model:, ttl: nil, instructions: nil, provider: nil, context: nil, with: nil)
|
|
89
|
+
config = context&.config || RubyLLM.config
|
|
90
|
+
model_instance, provider_instance = Models.resolve(model, provider: provider, config: config)
|
|
91
|
+
|
|
92
|
+
provider_instance.cache_content(content, model: model_instance, ttl:, instructions:, with:)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Fetches an existing cache resource by +name+ and returns a
|
|
96
|
+
# CachedContent. When +provider:+ is omitted, the provider of the
|
|
97
|
+
# configured default model is used.
|
|
98
|
+
#
|
|
99
|
+
# cache = RubyLLM::CachedContent.find("cachedContents/abc123", provider: :gemini)
|
|
100
|
+
#
|
|
101
|
+
def self.find(name, provider: nil, context: nil)
|
|
102
|
+
config = context&.config || RubyLLM.config
|
|
103
|
+
|
|
104
|
+
provider_instance = if provider
|
|
105
|
+
Provider.resolve!(provider).new(config)
|
|
106
|
+
else
|
|
107
|
+
Models.resolve(config.default_model, config:).last
|
|
108
|
+
end
|
|
109
|
+
provider_instance.find_cache(name)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Chat
|
|
5
|
+
module ToolConcurrency # :nodoc: all
|
|
6
|
+
MODES = %i[threads fibers].freeze
|
|
7
|
+
Result = Struct.new(:index, :tool_call, :value, :error, keyword_init: true)
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def run(mode, tool_calls, on_result: nil, &)
|
|
12
|
+
case mode
|
|
13
|
+
when :threads
|
|
14
|
+
run_with_threads(tool_calls, on_result:, &)
|
|
15
|
+
when :fibers
|
|
16
|
+
run_with_fibers(tool_calls, on_result:, &)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_with_threads(tool_calls, on_result:, &execute)
|
|
21
|
+
executor = rails_executor
|
|
22
|
+
workflow_context = Support::Instrumentation.current_workflow
|
|
23
|
+
queue = Queue.new
|
|
24
|
+
threads = tool_calls.each_value.with_index.map do |tool_call, index|
|
|
25
|
+
thread = Thread.new do
|
|
26
|
+
Support::Instrumentation.with_workflow(workflow_context) do
|
|
27
|
+
queue << capture_result(index, tool_call, executor, execute)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
thread.report_on_exception = false
|
|
31
|
+
thread
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
collect_results(queue, threads.size, on_result:)
|
|
35
|
+
ensure
|
|
36
|
+
threads&.each(&:join)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run_with_fibers(tool_calls, on_result:, &execute)
|
|
40
|
+
begin
|
|
41
|
+
require 'async'
|
|
42
|
+
require 'async/queue'
|
|
43
|
+
rescue LoadError
|
|
44
|
+
raise LoadError, "The 'async' gem is required for concurrent tool execution with fibers. " \
|
|
45
|
+
"Add `gem 'async', '>= 2.0'` to your Gemfile or use `concurrency: :threads`."
|
|
46
|
+
end
|
|
47
|
+
if Gem.loaded_specs.fetch('async').version < Gem::Version.new('2.0')
|
|
48
|
+
raise LoadError, "The 'async' gem version 2.0 or newer is required for concurrent tool execution with fibers."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
executor = rails_executor
|
|
52
|
+
workflow_context = Support::Instrumentation.current_workflow
|
|
53
|
+
Async do |task|
|
|
54
|
+
queue = Async::Queue.new
|
|
55
|
+
tasks = tool_calls.each_value.with_index.map do |tool_call, index|
|
|
56
|
+
task.async do
|
|
57
|
+
Support::Instrumentation.with_workflow(workflow_context) do
|
|
58
|
+
queue << capture_result(index, tool_call, executor, execute)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
collect_results(queue, tasks.size, on_result:)
|
|
64
|
+
ensure
|
|
65
|
+
tasks&.each(&:wait)
|
|
66
|
+
end.wait
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def collect_results(queue, count, on_result:)
|
|
70
|
+
results = Array.new(count)
|
|
71
|
+
errors = []
|
|
72
|
+
|
|
73
|
+
count.times do
|
|
74
|
+
result = queue.pop
|
|
75
|
+
if result.error
|
|
76
|
+
errors << result.error
|
|
77
|
+
else
|
|
78
|
+
results[result.index] = [result.tool_call, result.value]
|
|
79
|
+
on_result&.call(result.tool_call, result.value)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
raise errors.first if errors.any?
|
|
84
|
+
|
|
85
|
+
results
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def capture_result(index, tool_call, rails_executor, execute)
|
|
89
|
+
tool_call, value = run_tool_call(tool_call, rails_executor, execute)
|
|
90
|
+
Result.new(index:, tool_call:, value:)
|
|
91
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
92
|
+
Result.new(index:, tool_call:, error: e)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_tool_call(tool_call, rails_executor, execute)
|
|
96
|
+
if rails_executor
|
|
97
|
+
rails_executor.wrap { [tool_call, execute.call(tool_call)] }
|
|
98
|
+
else
|
|
99
|
+
[tool_call, execute.call(tool_call)]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def rails_executor
|
|
104
|
+
defined?(Rails) && Rails.respond_to?(:application) && Rails.application&.executor
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private_class_method :run_with_threads, :run_with_fibers, :collect_results, :capture_result, :run_tool_call,
|
|
108
|
+
:rails_executor
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|