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,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'io/wait'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Transport # :nodoc:
|
|
9
|
+
class WebsocketConnection # :nodoc: all
|
|
10
|
+
MAX_FRAME_BYTES = 16 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
attr_reader :url
|
|
13
|
+
|
|
14
|
+
def self.open(url, headers:, config:)
|
|
15
|
+
connection = new(url, headers:, config:)
|
|
16
|
+
connection.connect
|
|
17
|
+
yield connection
|
|
18
|
+
ensure
|
|
19
|
+
connection&.close
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(url, headers:, config:)
|
|
23
|
+
@url = url
|
|
24
|
+
@uri = URI.parse(url)
|
|
25
|
+
raise ArgumentError, 'WebSocket URL must use ws or wss' unless %w[ws wss].include?(@uri.scheme)
|
|
26
|
+
if @uri.userinfo || @uri.fragment
|
|
27
|
+
raise ArgumentError, 'WebSocket URL must not contain credentials or a fragment'
|
|
28
|
+
end
|
|
29
|
+
raise ArgumentError, 'WebSocket connections do not support HTTP proxies' if config.http_proxy
|
|
30
|
+
|
|
31
|
+
@headers = headers
|
|
32
|
+
@timeout = config.request_timeout
|
|
33
|
+
@messages = []
|
|
34
|
+
@write_lock = Mutex.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def connect
|
|
38
|
+
load_driver
|
|
39
|
+
deadline = monotonic_time + @timeout
|
|
40
|
+
open_socket(deadline)
|
|
41
|
+
configure_driver
|
|
42
|
+
@driver.start
|
|
43
|
+
read_frame(deadline) until @opened || @closed || @error
|
|
44
|
+
raise @error if @error
|
|
45
|
+
raise Error, 'WebSocket closed before opening' unless @opened
|
|
46
|
+
|
|
47
|
+
self
|
|
48
|
+
rescue StandardError
|
|
49
|
+
close
|
|
50
|
+
raise
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def send_text(text)
|
|
54
|
+
ensure_open
|
|
55
|
+
@driver.text(text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def send_binary(data)
|
|
59
|
+
ensure_open
|
|
60
|
+
@driver.binary(data.b)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read(timeout: @timeout)
|
|
64
|
+
deadline = monotonic_time + timeout
|
|
65
|
+
read_frame(deadline) while @messages.empty? && !@closed && !@error
|
|
66
|
+
raise @error if @error
|
|
67
|
+
|
|
68
|
+
@messages.shift
|
|
69
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
70
|
+
raise unless @closed
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read_available
|
|
74
|
+
ensure_open
|
|
75
|
+
until @messages.any? || @closed || @error
|
|
76
|
+
break unless read_available_frame
|
|
77
|
+
end
|
|
78
|
+
ensure_open
|
|
79
|
+
|
|
80
|
+
@messages.shift
|
|
81
|
+
rescue EOFError, Errno::ECONNRESET
|
|
82
|
+
@closed = true
|
|
83
|
+
raise Error, 'WebSocket connection ended without a close frame'
|
|
84
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
85
|
+
raise unless @closed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def each_message(write:)
|
|
89
|
+
writer_error = nil
|
|
90
|
+
writer = Thread.new do
|
|
91
|
+
write.call(self)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
writer_error = e
|
|
94
|
+
close
|
|
95
|
+
end
|
|
96
|
+
writer.report_on_exception = false
|
|
97
|
+
|
|
98
|
+
while (message = read)
|
|
99
|
+
yield message
|
|
100
|
+
end
|
|
101
|
+
finish_writer(writer)
|
|
102
|
+
raise writer_error if writer_error
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
raise writer_error || e
|
|
105
|
+
ensure
|
|
106
|
+
close
|
|
107
|
+
writer.kill.join if writer&.alive?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def close
|
|
111
|
+
@driver.close if @driver && @opened && !@closed
|
|
112
|
+
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError, Error
|
|
113
|
+
nil
|
|
114
|
+
ensure
|
|
115
|
+
@closed = true
|
|
116
|
+
close_socket
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def write(data)
|
|
120
|
+
@write_lock.synchronize do
|
|
121
|
+
deadline = monotonic_time + @timeout
|
|
122
|
+
offset = 0
|
|
123
|
+
while offset < data.bytesize
|
|
124
|
+
written = socket_operation(deadline) { @socket.write_nonblock(data.byteslice(offset..)) }
|
|
125
|
+
offset += written
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
data.bytesize
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def read_available_frame
|
|
134
|
+
data = @socket.read_nonblock(16_384, exception: false)
|
|
135
|
+
return if %i[wait_readable wait_writable].include?(data)
|
|
136
|
+
|
|
137
|
+
raise EOFError unless data
|
|
138
|
+
|
|
139
|
+
@driver.parse(data)
|
|
140
|
+
data
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def finish_writer(writer)
|
|
144
|
+
raise Error, 'WebSocket closed before outgoing messages finished' unless writer.join(1)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def configure_driver
|
|
148
|
+
@driver = WebSocket::Driver.client(self, max_length: MAX_FRAME_BYTES)
|
|
149
|
+
@headers.each { |name, value| @driver.set_header(name, value) if value }
|
|
150
|
+
@driver.on(:open) { @opened = true }
|
|
151
|
+
@driver.on(:message) { |event| @messages << event.data }
|
|
152
|
+
@driver.on(:error) { |event| @error = Error.new(event.message) }
|
|
153
|
+
@driver.on(:close) do |event|
|
|
154
|
+
@closed = true
|
|
155
|
+
@error ||= Error.new("WebSocket closed with code #{event.code}: #{event.reason}") unless event.code == 1000
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def open_socket(deadline)
|
|
160
|
+
port = @uri.port || (@uri.scheme == 'wss' ? 443 : 80)
|
|
161
|
+
@socket = Socket.tcp(@uri.host, port, connect_timeout: @timeout)
|
|
162
|
+
secure_socket(deadline) if @uri.scheme == 'wss'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def close_socket
|
|
166
|
+
@socket.close if @socket && !@socket.closed?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def load_driver
|
|
170
|
+
require 'websocket/driver'
|
|
171
|
+
rescue LoadError
|
|
172
|
+
raise LoadError, 'WebSocket streaming requires websocket-driver. Add gem "websocket-driver" to your Gemfile.'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def secure_socket(deadline)
|
|
176
|
+
context = OpenSSL::SSL::SSLContext.new
|
|
177
|
+
context.set_params
|
|
178
|
+
socket = OpenSSL::SSL::SSLSocket.new(@socket, context)
|
|
179
|
+
socket.hostname = @uri.host
|
|
180
|
+
socket.sync_close = true
|
|
181
|
+
@socket = socket
|
|
182
|
+
socket_operation(deadline) { @socket.connect_nonblock }
|
|
183
|
+
@socket.post_connection_check(@uri.host)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def ensure_open
|
|
187
|
+
raise Error, 'WebSocket connection is closed' unless @opened && !@closed
|
|
188
|
+
raise @error if @error
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def read_frame(deadline)
|
|
192
|
+
data = socket_operation(deadline) { @socket.read_nonblock(16_384) }
|
|
193
|
+
@driver.parse(data)
|
|
194
|
+
rescue EOFError, Errno::ECONNRESET
|
|
195
|
+
@closed = true
|
|
196
|
+
@error ||= Error.new('WebSocket connection ended without a close frame')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def socket_operation(deadline)
|
|
200
|
+
yield
|
|
201
|
+
rescue IO::WaitReadable
|
|
202
|
+
wait_for_socket(:wait_readable, deadline)
|
|
203
|
+
retry
|
|
204
|
+
rescue IO::WaitWritable
|
|
205
|
+
wait_for_socket(:wait_writable, deadline)
|
|
206
|
+
retry
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def wait_for_socket(method, deadline)
|
|
210
|
+
remaining = deadline - monotonic_time
|
|
211
|
+
raise Error, 'WebSocket operation timed out' unless remaining.positive?
|
|
212
|
+
raise Error, 'WebSocket operation timed out' unless @socket.to_io.public_send(method, remaining)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def monotonic_time
|
|
216
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# An UploadedFile is the metadata record for a file stored with a provider
|
|
5
|
+
# through its Files API. Upload a file once with ::upload, then reuse its
|
|
6
|
+
# provider id or URI, for example as a chat attachment or in a batch.
|
|
7
|
+
#
|
|
8
|
+
# file = RubyLLM.upload("batch.jsonl", purpose: "batch")
|
|
9
|
+
# file.id # => "file_..."
|
|
10
|
+
# file.filename # => "batch.jsonl"
|
|
11
|
+
# file.byte_size # => 1234
|
|
12
|
+
#
|
|
13
|
+
# File ids are provider-owned. Persist #provider alongside #id and pass it
|
|
14
|
+
# back when finding or downloading the file later.
|
|
15
|
+
class UploadedFile
|
|
16
|
+
include Support::Inspectable
|
|
17
|
+
|
|
18
|
+
def inspect_attributes # :nodoc:
|
|
19
|
+
{ id: id, provider: provider, filename: filename, byte_size: byte_size }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The provider-assigned file identifier, such as <tt>"file_..."</tt>.
|
|
23
|
+
attr_reader :id
|
|
24
|
+
|
|
25
|
+
# The slug of the provider that stores the file.
|
|
26
|
+
attr_reader :provider
|
|
27
|
+
|
|
28
|
+
# The filename reported by the provider.
|
|
29
|
+
attr_reader :filename
|
|
30
|
+
|
|
31
|
+
# The file size in bytes.
|
|
32
|
+
attr_reader :byte_size
|
|
33
|
+
|
|
34
|
+
# The Time the provider stored the file.
|
|
35
|
+
attr_reader :created_at
|
|
36
|
+
|
|
37
|
+
# The Time the provider will delete the file, or +nil+ if it does not
|
|
38
|
+
# expire.
|
|
39
|
+
attr_reader :expires_at
|
|
40
|
+
|
|
41
|
+
# The provider-reported processing status of the file.
|
|
42
|
+
attr_reader :status
|
|
43
|
+
|
|
44
|
+
# The MIME type of the stored file.
|
|
45
|
+
attr_reader :mime_type
|
|
46
|
+
|
|
47
|
+
# The purpose the file was uploaded for, such as <tt>"batch"</tt>, when
|
|
48
|
+
# the provider tracks one.
|
|
49
|
+
attr_reader :purpose
|
|
50
|
+
|
|
51
|
+
# The provider URI for the file, such as a Gemini Files API URI or a
|
|
52
|
+
# <tt>gs://</tt> or <tt>s3://</tt> location for storage-backed providers.
|
|
53
|
+
attr_reader :uri
|
|
54
|
+
|
|
55
|
+
# Whether the provider allows downloading the file's content.
|
|
56
|
+
attr_reader :downloadable
|
|
57
|
+
|
|
58
|
+
# The raw provider response data for the file, as a Hash.
|
|
59
|
+
attr_reader :metadata
|
|
60
|
+
|
|
61
|
+
def initialize(id:, **attributes) # :nodoc:
|
|
62
|
+
@id = id
|
|
63
|
+
@provider = attributes[:provider]
|
|
64
|
+
@filename = attributes[:filename]
|
|
65
|
+
@byte_size = attributes[:byte_size]
|
|
66
|
+
@created_at = attributes[:created_at]
|
|
67
|
+
@expires_at = attributes[:expires_at]
|
|
68
|
+
@status = attributes[:status]
|
|
69
|
+
@mime_type = attributes[:mime_type]
|
|
70
|
+
@purpose = attributes[:purpose]
|
|
71
|
+
@uri = attributes[:uri]
|
|
72
|
+
@downloadable = attributes[:downloadable]
|
|
73
|
+
@metadata = attributes[:metadata] || {}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
EXPIRY_MARGIN = 60 # :nodoc:
|
|
77
|
+
|
|
78
|
+
# Returns +true+ once the provider's retention window for this file has
|
|
79
|
+
# passed or is about to; a file expiring within the next minute cannot
|
|
80
|
+
# safely serve a request. Files without a reported expiry never expire.
|
|
81
|
+
def expired?
|
|
82
|
+
!expires_at.nil? && expires_at <= Time.now + EXPIRY_MARGIN
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Uploads +file+ to the provider's Files API and returns an UploadedFile.
|
|
86
|
+
# +file+ may be a path, an IO object, or an Attachment. When +provider:+
|
|
87
|
+
# is omitted, the provider of the configured default model is used.
|
|
88
|
+
# Also available as RubyLLM.upload.
|
|
89
|
+
#
|
|
90
|
+
# RubyLLM::UploadedFile.upload("document.pdf", provider: :anthropic)
|
|
91
|
+
# RubyLLM::UploadedFile.upload(io, provider: :openai, purpose: "batch",
|
|
92
|
+
# filename: "batch.jsonl")
|
|
93
|
+
#
|
|
94
|
+
# OpenAI and Azure require +purpose:+. Cohere uses +purpose:+ for its
|
|
95
|
+
# dataset type, such as <tt>"embed-input"</tt>.
|
|
96
|
+
# Pass +expires_in:+ as a number of
|
|
97
|
+
# seconds to have the provider delete the file automatically; OpenAI,
|
|
98
|
+
# xAI, and Mistral support it, and Mistral rounds up to whole hours.
|
|
99
|
+
# Storage-backed providers (Vertex AI and Bedrock) store the file at
|
|
100
|
+
# +uri:+ when given, otherwise at a generated object name under the
|
|
101
|
+
# configured bucket. +content_type:+ overrides the MIME type RubyLLM
|
|
102
|
+
# detects. +provider_options:+ carries the rest in the provider's own
|
|
103
|
+
# vocabulary: +visibility:+ on Mistral, +display_name:+ on Gemini.
|
|
104
|
+
def self.upload(file, provider: nil, context: nil, filename: nil, purpose: nil, expires_in: nil,
|
|
105
|
+
uri: nil, content_type: nil, provider_options: {})
|
|
106
|
+
options = { filename:, purpose:, expires_in:, uri:, content_type: }.compact
|
|
107
|
+
options[:provider_options] = provider_options unless provider_options.empty?
|
|
108
|
+
provider_for(provider, context).upload_file(file, **options)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Fetches metadata for an existing provider file by +id+ and returns an
|
|
112
|
+
# UploadedFile. When +provider:+ is omitted, the provider of the
|
|
113
|
+
# configured default model is used.
|
|
114
|
+
#
|
|
115
|
+
# file = RubyLLM::UploadedFile.find("file_123")
|
|
116
|
+
#
|
|
117
|
+
def self.find(id, provider: nil, context: nil)
|
|
118
|
+
provider_for(provider, context).find_file(id)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Downloads the provider file +id+ and returns a DownloadedFile.
|
|
122
|
+
# Also available as RubyLLM.download.
|
|
123
|
+
#
|
|
124
|
+
# RubyLLM.download(file.id, provider: :openai).save("report.pdf")
|
|
125
|
+
#
|
|
126
|
+
# Not every provider allows downloads; see #downloadable. Cohere
|
|
127
|
+
# downloads original uploaded bytes when preserved; generated datasets
|
|
128
|
+
# return JSONL and require the optional +avro+ gem.
|
|
129
|
+
def self.download(id, provider: nil, context: nil)
|
|
130
|
+
DownloadedFile.new(provider_for(provider, context).download_file(id))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.provider_for(provider, context)
|
|
134
|
+
config = context&.config || RubyLLM.config
|
|
135
|
+
|
|
136
|
+
if provider
|
|
137
|
+
Provider.resolve!(provider).new(config)
|
|
138
|
+
else
|
|
139
|
+
Models.resolve(config.default_model, config:).last
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
private_class_method :provider_for
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/ruby_llm/version.rb
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A Video is a generated clip. Save it with #save or read the raw bytes
|
|
5
|
+
# with #to_blob. Both handle hosted URLs and inline data.
|
|
6
|
+
#
|
|
7
|
+
# video = RubyLLM.animate("a paper boat sailing down a rainy gutter")
|
|
8
|
+
# video.save("boat.mp4")
|
|
9
|
+
#
|
|
10
|
+
class Video
|
|
11
|
+
include Support::Inspectable
|
|
12
|
+
|
|
13
|
+
# The URL of the hosted video, for providers that return one, or +nil+.
|
|
14
|
+
attr_reader :url
|
|
15
|
+
|
|
16
|
+
# The raw video bytes, returned inline or downloaded with the
|
|
17
|
+
# provider's credentials, or +nil+ when the video is hosted at #url.
|
|
18
|
+
attr_reader :data
|
|
19
|
+
|
|
20
|
+
# The MIME type of the video, such as <tt>"video/mp4"</tt>.
|
|
21
|
+
attr_reader :mime_type
|
|
22
|
+
|
|
23
|
+
# The id of the model that generated the video.
|
|
24
|
+
attr_reader :model
|
|
25
|
+
|
|
26
|
+
# The clip length in seconds, when the provider reports one.
|
|
27
|
+
attr_reader :duration
|
|
28
|
+
|
|
29
|
+
# The provider's raw job response, for provider-specific fields such
|
|
30
|
+
# as reported cost.
|
|
31
|
+
attr_reader :raw
|
|
32
|
+
|
|
33
|
+
# Generates a video from +prompt+, blocks until the provider finishes
|
|
34
|
+
# rendering it, and returns a Video. Most code calls this through
|
|
35
|
+
# RubyLLM.animate. Video generation is asynchronous on every provider,
|
|
36
|
+
# so this submits a job with VideoJob.animate_later and polls until it
|
|
37
|
+
# is done, honoring <tt>config.video_generation_timeout</tt> and
|
|
38
|
+
# <tt>config.video_generation_poll_interval</tt>.
|
|
39
|
+
#
|
|
40
|
+
# +model:+ selects the video model and defaults to the configured
|
|
41
|
+
# +default_video_model+. +provider:+ forces a specific provider, and
|
|
42
|
+
# +assume_model_exists:+ skips the registry lookup. +with:+ passes a
|
|
43
|
+
# reference image or a video to edit on models that support it. Models
|
|
44
|
+
# driven by image and audio input can omit the prompt and pass both
|
|
45
|
+
# attachments through +with:+.
|
|
46
|
+
# +extend:+ continues a source video instead. It accepts a Video,
|
|
47
|
+
# file path, URL, or Attachment and cannot be combined with +with:+.
|
|
48
|
+
# +provider_options:+ takes options in the provider's request
|
|
49
|
+
# vocabulary, such as durations and resolutions, and merges them into
|
|
50
|
+
# the request as-is. +context:+ supplies a Context whose configuration
|
|
51
|
+
# replaces the global one. +metadata:+ is included in the
|
|
52
|
+
# instrumentation payload.
|
|
53
|
+
#
|
|
54
|
+
# video = RubyLLM.animate("a hummingbird in slow motion", model: "veo-3.1-fast-generate-preview")
|
|
55
|
+
#
|
|
56
|
+
# RubyLLM.animate(
|
|
57
|
+
# "Make the waterfall crash down",
|
|
58
|
+
# model: "grok-imagine-video-1.5",
|
|
59
|
+
# with: "waterfall.png",
|
|
60
|
+
# provider_options: { duration: 5 }
|
|
61
|
+
# )
|
|
62
|
+
#
|
|
63
|
+
def self.animate(prompt = nil,
|
|
64
|
+
model: nil,
|
|
65
|
+
provider: nil,
|
|
66
|
+
assume_model_exists: false,
|
|
67
|
+
context: nil,
|
|
68
|
+
with: nil,
|
|
69
|
+
extend: nil,
|
|
70
|
+
provider_options: {},
|
|
71
|
+
metadata: nil)
|
|
72
|
+
config = context&.config || RubyLLM.config
|
|
73
|
+
payload = { model:, prompt:, provider_options:, metadata: }
|
|
74
|
+
|
|
75
|
+
RubyLLM.instrument('video.ruby_llm', payload, config: config) do |event|
|
|
76
|
+
job = VideoJob.animate_later(prompt, model:, provider:, assume_model_exists:,
|
|
77
|
+
context:, with:, extend:, provider_options:, metadata:)
|
|
78
|
+
event[:model] = job.model
|
|
79
|
+
event[:job_id] = job.id
|
|
80
|
+
job.wait
|
|
81
|
+
result = job.video
|
|
82
|
+
event[:result] = result
|
|
83
|
+
event[:response_model] = result.model
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Set by the protocol that generated the video, so a Context's
|
|
89
|
+
# connection settings reach #to_blob.
|
|
90
|
+
attr_writer :config # :nodoc:
|
|
91
|
+
|
|
92
|
+
def config # :nodoc:
|
|
93
|
+
@config || RubyLLM.config
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def initialize(url: nil, data: nil, mime_type: nil, model: nil, duration: nil, raw: nil) # :nodoc:
|
|
97
|
+
@url = url
|
|
98
|
+
@data = data
|
|
99
|
+
@mime_type = mime_type
|
|
100
|
+
@model = model
|
|
101
|
+
@duration = duration
|
|
102
|
+
@raw = raw
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the raw binary video bytes, downloading from #url when the
|
|
106
|
+
# provider returned a hosted video.
|
|
107
|
+
#
|
|
108
|
+
# video_bytes = video.to_blob
|
|
109
|
+
#
|
|
110
|
+
def to_blob
|
|
111
|
+
data || Transport::Connection.basic(config).get(url).body
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Writes the binary video to +path+, expanding it first. Returns
|
|
115
|
+
# +path+ as given.
|
|
116
|
+
#
|
|
117
|
+
# video.save("clip.mp4")
|
|
118
|
+
#
|
|
119
|
+
def save(path)
|
|
120
|
+
File.binwrite(File.expand_path(path), to_blob)
|
|
121
|
+
path
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def inspect_attributes # :nodoc:
|
|
127
|
+
{
|
|
128
|
+
model: model,
|
|
129
|
+
mime_type: mime_type,
|
|
130
|
+
url: url,
|
|
131
|
+
data: data && "#{data.bytesize} bytes",
|
|
132
|
+
duration: duration
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A VideoJob is an in-flight video generation. RubyLLM.animate_later
|
|
5
|
+
# returns one immediately; poll it with #refresh and read the finished
|
|
6
|
+
# clip with #video. RubyLLM.animate runs the same job through #wait.
|
|
7
|
+
#
|
|
8
|
+
# job = RubyLLM.animate_later("a paper boat sailing down a gutter")
|
|
9
|
+
# job.wait
|
|
10
|
+
# job.video.save("boat.mp4")
|
|
11
|
+
#
|
|
12
|
+
class VideoJob
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
|
|
15
|
+
# The provider's id for the job.
|
|
16
|
+
attr_reader :id
|
|
17
|
+
|
|
18
|
+
# The job state: +:pending+, +:completed+, or +:failed+.
|
|
19
|
+
attr_reader :status
|
|
20
|
+
|
|
21
|
+
# The id of the model rendering the video.
|
|
22
|
+
attr_reader :model
|
|
23
|
+
|
|
24
|
+
# The provider's failure message when #failed?, or +nil+.
|
|
25
|
+
attr_reader :error
|
|
26
|
+
|
|
27
|
+
# The provider's raw response from the last submit or poll.
|
|
28
|
+
attr_reader :raw
|
|
29
|
+
|
|
30
|
+
# Submits a video generation job and returns a VideoJob without
|
|
31
|
+
# waiting for the result. Most code calls this through
|
|
32
|
+
# RubyLLM.animate_later. Takes the same arguments as Video.animate.
|
|
33
|
+
#
|
|
34
|
+
# job = RubyLLM.animate_later("a hummingbird in slow motion")
|
|
35
|
+
# job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
|
|
36
|
+
# job.done? # => false
|
|
37
|
+
#
|
|
38
|
+
def self.animate_later(prompt = nil,
|
|
39
|
+
model: nil,
|
|
40
|
+
provider: nil,
|
|
41
|
+
assume_model_exists: false,
|
|
42
|
+
context: nil,
|
|
43
|
+
with: nil,
|
|
44
|
+
extend: nil,
|
|
45
|
+
provider_options: {},
|
|
46
|
+
metadata: nil)
|
|
47
|
+
config = context&.config || RubyLLM.config
|
|
48
|
+
model ||= config.default_video_model
|
|
49
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
50
|
+
config: config)
|
|
51
|
+
payload = {
|
|
52
|
+
provider: provider_instance.slug,
|
|
53
|
+
provider_class: provider_instance.class.display_name,
|
|
54
|
+
model: model.id,
|
|
55
|
+
model_info: model,
|
|
56
|
+
prompt: prompt,
|
|
57
|
+
provider_options: provider_options,
|
|
58
|
+
metadata: metadata
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
RubyLLM.instrument('video_job.ruby_llm', payload, config: config) do |event|
|
|
62
|
+
job = provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
|
|
63
|
+
event[:job_id] = job.id
|
|
64
|
+
job
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil) # :nodoc:
|
|
69
|
+
@id = id
|
|
70
|
+
@protocol = protocol
|
|
71
|
+
@model = model
|
|
72
|
+
@status = status
|
|
73
|
+
@raw = raw
|
|
74
|
+
@error = error
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns +true+ while the provider is still rendering the video.
|
|
78
|
+
def pending?
|
|
79
|
+
status == :pending
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns +true+ once the job finished, successfully or not.
|
|
83
|
+
def done?
|
|
84
|
+
!pending?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns +true+ when the job finished with a video.
|
|
88
|
+
def completed?
|
|
89
|
+
status == :completed
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns +true+ when the job finished without a video. #error carries
|
|
93
|
+
# the provider's failure message.
|
|
94
|
+
def failed?
|
|
95
|
+
status == :failed
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Re-fetches the job from the provider, updating #status, #error, and
|
|
99
|
+
# #raw. Does nothing once the job is #done?. Returns self.
|
|
100
|
+
def refresh
|
|
101
|
+
return self if done?
|
|
102
|
+
|
|
103
|
+
state = @protocol.refresh_video_job(self)
|
|
104
|
+
@status = state.fetch(:status)
|
|
105
|
+
@raw = state[:raw]
|
|
106
|
+
@error = state[:error]
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Polls the job until it finishes, then returns self. Raises Error
|
|
111
|
+
# when the job fails or +timeout+ elapses first. +timeout+ and
|
|
112
|
+
# +interval+ are seconds and default to the configured
|
|
113
|
+
# +video_generation_timeout+ and +video_generation_poll_interval+.
|
|
114
|
+
def wait(timeout: nil, interval: nil)
|
|
115
|
+
timeout ||= @protocol.config.video_generation_timeout
|
|
116
|
+
interval ||= @protocol.config.video_generation_poll_interval
|
|
117
|
+
deadline = monotonic_time + timeout
|
|
118
|
+
|
|
119
|
+
until done?
|
|
120
|
+
raise Error, "Video generation timed out after #{timeout} seconds" if monotonic_time > deadline
|
|
121
|
+
|
|
122
|
+
sleep interval
|
|
123
|
+
refresh
|
|
124
|
+
end
|
|
125
|
+
raise Error, "Video generation failed: #{error}" if failed?
|
|
126
|
+
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns the finished Video, downloading it from the provider when
|
|
131
|
+
# its API requires that. Returns +nil+ while #pending? and raises
|
|
132
|
+
# Error when #failed?.
|
|
133
|
+
def video
|
|
134
|
+
raise Error, "Video generation failed: #{error}" if failed?
|
|
135
|
+
return unless completed?
|
|
136
|
+
|
|
137
|
+
@video ||= @protocol.download_video(self).tap { |video| video.config = @protocol.config }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def monotonic_time
|
|
143
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def inspect_attributes # :nodoc:
|
|
147
|
+
{ id: id, status: status, model: model }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|