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,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
class Flows
|
|
7
|
+
module Videos # :nodoc: all
|
|
8
|
+
REFERENCE_VIDEO_MODELS = %w[
|
|
9
|
+
bytedance-seedance-v2 bytedance-seedance-v2-fast bytedance-seedance-v2-mini bytedance-seedance-v2.5
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def video_url
|
|
13
|
+
'v1/flows/video'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_video_payload(prompt, model:, with: [], provider_options: {})
|
|
17
|
+
payload = if model == 'creatify-aurora'
|
|
18
|
+
render_character_animation(prompt, with:)
|
|
19
|
+
else
|
|
20
|
+
render_video_inputs(prompt, model:, with:)
|
|
21
|
+
end
|
|
22
|
+
Support::Utils.deep_merge({ model_id: model }.merge(payload), provider_options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_video_inputs(prompt, model:, with:)
|
|
26
|
+
raise ArgumentError, 'This ElevenLabs video model requires a prompt' if prompt.to_s.empty?
|
|
27
|
+
|
|
28
|
+
payload = { prompt: }
|
|
29
|
+
if with.all?(&:image?)
|
|
30
|
+
raise ArgumentError, 'Video generation accepts at most two frame images' if with.size > 2
|
|
31
|
+
|
|
32
|
+
payload[:start_frame] = render_media_reference(with[0]) if with[0]
|
|
33
|
+
payload[:end_frame] = render_media_reference(with[1]) if with[1]
|
|
34
|
+
else
|
|
35
|
+
render_video_references(payload, with, model:)
|
|
36
|
+
end
|
|
37
|
+
payload
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_character_animation(prompt, with:)
|
|
41
|
+
unless prompt.nil? || prompt.empty?
|
|
42
|
+
raise ArgumentError, 'Creatify Aurora uses image and audio input; omit the prompt'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
image = with.select(&:image?)
|
|
46
|
+
audio = with.select(&:audio?)
|
|
47
|
+
unless with.size == 2 && image.one? && audio.one?
|
|
48
|
+
raise ArgumentError, 'Creatify Aurora requires exactly one image and one audio attachment'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{ image: render_media_reference(image.first), audio: render_media_reference(audio.first) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_video_references(payload, attachments, model:)
|
|
55
|
+
unless REFERENCE_VIDEO_MODELS.include?(model)
|
|
56
|
+
raise ArgumentError, 'This ElevenLabs video model only accepts image attachments'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
{ images: :image?, audios: :audio?, videos: :video? }.each do |field, predicate|
|
|
60
|
+
references = attachments.select { |attachment| attachment.public_send(predicate) }
|
|
61
|
+
payload[field] = references.map { |attachment| render_media_reference(attachment) } if references.any?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_video_job(response, model:)
|
|
66
|
+
VideoJob.new(id: response.body.fetch('id'), protocol: self, model:, raw: response.body)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def video_job_url(job)
|
|
70
|
+
"#{video_url}/#{job.id}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_video_job_status(response, **)
|
|
74
|
+
parse_generation_status(response.body)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def download_video(job)
|
|
78
|
+
Video.new(url: job.raw.fetch('content_url'), mime_type: job.raw.fetch('content_mime_type'),
|
|
79
|
+
model: job.model, raw: job.raw)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def validate_animate_inputs!(with:)
|
|
83
|
+
with.each do |attachment|
|
|
84
|
+
unless attachment.image? || attachment.audio? || attachment.video?
|
|
85
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
# The asynchronous ElevenLabs Image & Video API.
|
|
7
|
+
class Flows < Protocol
|
|
8
|
+
include Flows::Media
|
|
9
|
+
include Flows::Images
|
|
10
|
+
include Flows::Videos
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
# Model catalog for ElevenLabs. GET v1/models returns a bare array of
|
|
7
|
+
# the speech synthesis models and leaves out the Scribe transcription
|
|
8
|
+
# models, so those are appended from the published catalog.
|
|
9
|
+
module Models
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
TRANSCRIPTION_MODELS = {
|
|
13
|
+
'scribe_v2' => 'Scribe v2',
|
|
14
|
+
'scribe_v2_realtime' => 'Scribe v2 Realtime'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def models_url
|
|
18
|
+
'v1/models'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parse_list_models_response(response, slug)
|
|
22
|
+
listed = Array(response.body).map do |data|
|
|
23
|
+
build_model(data['model_id'], slug, name: data['name'], description: data['description'], data: data)
|
|
24
|
+
end
|
|
25
|
+
listed_ids = listed.map(&:id)
|
|
26
|
+
|
|
27
|
+
listed + TRANSCRIPTION_MODELS.except(*listed_ids).map do |model_id, name|
|
|
28
|
+
build_model(model_id, slug, name: name, transcription: true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_model(model_id, slug, name: nil, description: nil, data: {}, transcription: false)
|
|
33
|
+
transcription ||= TRANSCRIPTION_MODELS.key?(model_id)
|
|
34
|
+
|
|
35
|
+
Model.new(
|
|
36
|
+
id: model_id,
|
|
37
|
+
name: name || model_id,
|
|
38
|
+
provider: slug,
|
|
39
|
+
family: transcription ? 'scribe' : 'eleven',
|
|
40
|
+
modalities: modalities_for(data, transcription),
|
|
41
|
+
capabilities: capabilities_from(data, transcription),
|
|
42
|
+
pricing: {},
|
|
43
|
+
metadata: { description: description }.compact
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def modalities_for(data, transcription)
|
|
48
|
+
return { input: ['audio'], output: ['text'] } if transcription
|
|
49
|
+
return { input: ['audio'], output: ['audio'] } if data['can_do_voice_conversion']
|
|
50
|
+
|
|
51
|
+
{ input: ['text'], output: ['audio'] }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def capabilities_from(data, transcription)
|
|
55
|
+
capabilities = []
|
|
56
|
+
capabilities << 'transcription' if transcription
|
|
57
|
+
capabilities << 'speech_generation' if data['can_do_text_to_speech']
|
|
58
|
+
capabilities << 'fine_tuning' if data['can_be_finetuned']
|
|
59
|
+
capabilities
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
# Speech dialect for the ElevenLabs text-to-speech API. The voice id
|
|
7
|
+
# is a path segment and the container is an output_format query
|
|
8
|
+
# value, so the endpoint cannot be named without the voice and format
|
|
9
|
+
# the caller asked for. The response is raw audio bytes.
|
|
10
|
+
module Speech
|
|
11
|
+
DEFAULT_VOICE = 'JBFqnCBsd6RMkjVDRZzb'
|
|
12
|
+
DEFAULT_OUTPUT_FORMAT = 'mp3_44100_128'
|
|
13
|
+
|
|
14
|
+
OUTPUT_FORMATS = {
|
|
15
|
+
'alaw' => 'alaw_8000',
|
|
16
|
+
'mp3' => 'mp3_44100_128',
|
|
17
|
+
'opus' => 'opus_48000_128',
|
|
18
|
+
'pcm' => 'pcm_44100',
|
|
19
|
+
'ulaw' => 'ulaw_8000',
|
|
20
|
+
'wav' => 'wav_44100'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def speak(input, model:, voice:, format:, provider_options: {}, &block)
|
|
24
|
+
track_usage(:speech) do
|
|
25
|
+
payload = render_speech_payload(input, model:, voice:, format:, provider_options:)
|
|
26
|
+
if block
|
|
27
|
+
next stream_speech_response(speech_url(voice:, format:, streaming: true), payload,
|
|
28
|
+
model:, voice:, format:, &block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
response = @connection.post speech_url(voice:, format:), payload, usage: @usage_tracker
|
|
32
|
+
parse_speech_response(response, model:, voice:, format:)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def speech_url(voice: nil, format: nil, streaming: false)
|
|
37
|
+
path = "v1/text-to-speech/#{voice || DEFAULT_VOICE}"
|
|
38
|
+
path += '/stream' if streaming
|
|
39
|
+
"#{path}?output_format=#{output_format_for(format)}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_speech_payload(input, model:, voice: nil, format: nil, provider_options: {}) # rubocop:disable Lint/UnusedMethodArgument
|
|
43
|
+
Support::Utils.deep_merge({ text: input, model_id: model }, provider_options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def parse_speech_response(response, model:, voice:, format:)
|
|
47
|
+
RubyLLM::Speech.new(
|
|
48
|
+
data: response.body,
|
|
49
|
+
model: model,
|
|
50
|
+
voice: voice || DEFAULT_VOICE,
|
|
51
|
+
format: container_for(format)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def output_format_for(format)
|
|
56
|
+
return DEFAULT_OUTPUT_FORMAT unless format
|
|
57
|
+
|
|
58
|
+
OUTPUT_FORMATS.fetch(format.to_s, format.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def container_for(format)
|
|
62
|
+
output_format_for(format).split('_').first
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
module StreamingTranscription # :nodoc: all
|
|
7
|
+
SAMPLE_RATES = [8000, 16_000, 22_050, 24_000, 44_100, 48_000].freeze
|
|
8
|
+
|
|
9
|
+
def stream_transcription(payload, model:, &block)
|
|
10
|
+
validate_streaming_transcription(payload)
|
|
11
|
+
audio = RubyLLM::Transcription::WavAudio.new(payload.fetch(:file).io.read)
|
|
12
|
+
raise ArgumentError, 'Streaming transcription requires non-empty audio' if audio.data.empty?
|
|
13
|
+
|
|
14
|
+
audio_format = streaming_audio_format(audio)
|
|
15
|
+
url = streaming_transcription_url(payload, audio_format:)
|
|
16
|
+
@usage_tracker.start
|
|
17
|
+
expected_commits = (audio.data.bytesize.to_f / transcription_segment_bytes(audio)).ceil
|
|
18
|
+
transcripts = collect_transcription(url, audio, expected_commits, &block)
|
|
19
|
+
result = build_streaming_transcription(transcripts, audio, model:)
|
|
20
|
+
block.call(TranscriptionChunk.new(type: TranscriptionChunk::DONE, text: result.text, raw: transcripts.last))
|
|
21
|
+
result
|
|
22
|
+
ensure
|
|
23
|
+
payload[:file]&.io&.close
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collect_transcription(url, audio, expected_commits, &block)
|
|
27
|
+
transcripts = []
|
|
28
|
+
commits = Queue.new
|
|
29
|
+
Transport::WebsocketConnection.open(url, headers: @provider.headers, config: @config) do |socket|
|
|
30
|
+
write = ->(connection) { send_transcription_audio(connection, audio, commits) }
|
|
31
|
+
socket.each_message(write:) do |message|
|
|
32
|
+
event = JSON.parse(message)
|
|
33
|
+
transcript = process_transcription_event(event, prefix: transcripts.any?, &block)
|
|
34
|
+
next unless transcript
|
|
35
|
+
|
|
36
|
+
transcripts << transcript
|
|
37
|
+
commits << true
|
|
38
|
+
socket.close if transcripts.size == expected_commits
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
unless transcripts.size == expected_commits && transcripts.any?
|
|
42
|
+
raise Error, 'ElevenLabs transcription ended before its committed transcript'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
transcripts
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_streaming_transcription(transcripts, audio, model:)
|
|
49
|
+
RubyLLM::Transcription.new(
|
|
50
|
+
text: transcripts.map { |item| item.fetch('text') }.join(' '), model:,
|
|
51
|
+
duration: audio.duration, language: transcripts.last['language_code'],
|
|
52
|
+
words: transcripts.flat_map { |item| item['words'] || [] }
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_streaming_transcription(payload)
|
|
57
|
+
unsupported = payload.keys & %i[diarize num_speakers temperature timestamps_granularity]
|
|
58
|
+
unless unsupported.empty?
|
|
59
|
+
raise ArgumentError, "ElevenLabs streaming transcription does not accept #{unsupported.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if payload.fetch(:commit_strategy, 'manual') != 'manual' || payload[:include_timestamps] == false ||
|
|
63
|
+
payload[:filter_background_audio]
|
|
64
|
+
raise ArgumentError, 'ElevenLabs file streaming requires manual commits and word timestamps'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def streaming_audio_format(audio)
|
|
69
|
+
return "pcm_#{audio.sample_rate}" if pcm_audio?(audio)
|
|
70
|
+
return 'ulaw_8000' if [audio.channels, audio.encoding, audio.sample_rate,
|
|
71
|
+
audio.bits_per_sample] == [1, 7, 8000, 8]
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, 'ElevenLabs streaming requires mono 16-bit PCM WAV or 8 kHz mu-law WAV audio'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def pcm_audio?(audio)
|
|
77
|
+
[audio.channels, audio.encoding, audio.bits_per_sample] == [1, 1, 16] &&
|
|
78
|
+
SAMPLE_RATES.include?(audio.sample_rate)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def streaming_transcription_url(payload, audio_format:)
|
|
82
|
+
language_detection = payload.fetch(:include_language_detection, true)
|
|
83
|
+
params = payload.except(:file).merge(audio_format:, commit_strategy: 'manual', include_timestamps: true,
|
|
84
|
+
include_language_detection: language_detection)
|
|
85
|
+
uri = URI.join("#{@provider.api_base.sub(%r{/+\z}, '')}/",
|
|
86
|
+
"v1/speech-to-text/realtime?#{URI.encode_www_form(params)}")
|
|
87
|
+
uri.scheme = uri.scheme == 'https' ? 'wss' : 'ws'
|
|
88
|
+
uri.to_s
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def transcription_segment_bytes(audio)
|
|
92
|
+
audio.sample_rate * audio.channels * audio.bits_per_sample / 8 * 20
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def send_transcription_audio(socket, audio, commits)
|
|
96
|
+
offset = 0
|
|
97
|
+
segment_bytes = transcription_segment_bytes(audio)
|
|
98
|
+
while offset < audio.data.bytesize
|
|
99
|
+
length = [16_384, segment_bytes - (offset % segment_bytes)].min
|
|
100
|
+
data = audio.data.byteslice(offset, length)
|
|
101
|
+
offset += data.bytesize
|
|
102
|
+
commit = (offset % segment_bytes).zero? || offset == audio.data.bytesize
|
|
103
|
+
socket.send_text(JSON.generate(message_type: 'input_audio_chunk',
|
|
104
|
+
'audio_base_64' => Base64.strict_encode64(data),
|
|
105
|
+
sample_rate: audio.sample_rate, commit:))
|
|
106
|
+
commits.pop if commit
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def process_transcription_event(event, prefix: false)
|
|
111
|
+
case event['message_type']
|
|
112
|
+
when 'partial_transcript'
|
|
113
|
+
yield TranscriptionChunk.new(type: TranscriptionChunk::PARTIAL, text: event['text'], raw: event)
|
|
114
|
+
when 'committed_transcript'
|
|
115
|
+
text = event['text']
|
|
116
|
+
yield TranscriptionChunk.new(type: TranscriptionChunk::DELTA, delta: prefix ? " #{text}" : text, raw: event)
|
|
117
|
+
when 'committed_transcript_with_timestamps'
|
|
118
|
+
return event
|
|
119
|
+
else
|
|
120
|
+
raise Error, event['error'] if event['error']
|
|
121
|
+
end
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
# Transcription dialect for the ElevenLabs speech-to-text API. The
|
|
7
|
+
# request is multipart with the model id in the form, and giving
|
|
8
|
+
# speaker names turns on diarization and caps the speaker count at
|
|
9
|
+
# the number of names.
|
|
10
|
+
module Transcription
|
|
11
|
+
def render_transcription_options(timestamps:, format:, streaming:)
|
|
12
|
+
return {} if timestamps.nil?
|
|
13
|
+
|
|
14
|
+
value = timestamps.to_s
|
|
15
|
+
allowed = streaming ? ['word'] : %w[none word character]
|
|
16
|
+
unless allowed.include?(value) && (format.nil? || format == value)
|
|
17
|
+
raise ArgumentError, "ElevenLabs timestamps must be #{allowed.join(', ')} and match format when provided"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
streaming ? { include_timestamps: true } : { timestamps_granularity: value }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
24
|
+
def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
|
|
25
|
+
speaker_references: nil, provider_options: {}, prompt: nil,
|
|
26
|
+
temperature: nil)
|
|
27
|
+
payload = {
|
|
28
|
+
model_id: model,
|
|
29
|
+
file: file_part,
|
|
30
|
+
language_code: language,
|
|
31
|
+
temperature: temperature,
|
|
32
|
+
timestamps_granularity: format
|
|
33
|
+
}.compact
|
|
34
|
+
|
|
35
|
+
if speaker_names
|
|
36
|
+
payload[:diarize] = true
|
|
37
|
+
payload[:num_speakers] = speaker_names.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
payload.merge(provider_options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def transcription_url
|
|
44
|
+
'v1/speech-to-text'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_transcription_response(response, model:)
|
|
48
|
+
data = response.body
|
|
49
|
+
|
|
50
|
+
RubyLLM::Transcription.new(
|
|
51
|
+
text: data['text'],
|
|
52
|
+
model: model,
|
|
53
|
+
language: data['language_code'],
|
|
54
|
+
duration: data['audio_duration_secs'],
|
|
55
|
+
words: data['words']
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The ElevenLabs audio API: text to speech, speech to text, and the model
|
|
6
|
+
# catalog behind them. Image and video generation and workspace assets use
|
|
7
|
+
# separate protocols.
|
|
8
|
+
class ElevenLabs < Protocol
|
|
9
|
+
include ElevenLabs::Models
|
|
10
|
+
include ElevenLabs::Speech
|
|
11
|
+
include ElevenLabs::Transcription
|
|
12
|
+
include ElevenLabs::StreamingTranscription
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Protocols
|
|
7
|
+
# Provider-managed file storage APIs.
|
|
8
|
+
class Files < Protocol
|
|
9
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
10
|
+
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil, provider_options: {})
|
|
11
|
+
attachment = file_attachment(file, filename:)
|
|
12
|
+
options = { purpose:, expires_in: }.compact.merge(provider_options.transform_keys(&:to_sym))
|
|
13
|
+
response = @connection.post(files_url, render_upload_payload(attachment, **options),
|
|
14
|
+
idempotent: false) do |request|
|
|
15
|
+
request.headers.delete('Content-Type')
|
|
16
|
+
upload_headers(request)
|
|
17
|
+
end
|
|
18
|
+
parse_file_response(response.body)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find(file_id)
|
|
22
|
+
response = @connection.get(file_info_url(file_id)) { |request| file_headers(request) }
|
|
23
|
+
parse_file_response(response.body)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def download(file_id)
|
|
27
|
+
response = @connection.get(download_file_url(file_id)) do |request|
|
|
28
|
+
request.headers['Accept'] = 'application/octet-stream'
|
|
29
|
+
file_headers(request)
|
|
30
|
+
end
|
|
31
|
+
response.body
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def list_uris(_uri)
|
|
35
|
+
raise Error, "#{@provider.slug} doesn't support file listing"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def files_url
|
|
41
|
+
'files'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def file_info_url(file_id)
|
|
45
|
+
"#{files_url}/#{file_id}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def download_file_url(file_id)
|
|
49
|
+
"#{file_info_url(file_id)}/content"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
53
|
+
def render_upload_payload(attachment, purpose: nil, expires_in: nil, visibility: nil,
|
|
54
|
+
display_name: nil, uri: nil, content_type: nil)
|
|
55
|
+
{ file: file_part(attachment) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def multipart_payload(attachment, **fields)
|
|
59
|
+
{ file: file_part(attachment) }.merge(fields.compact)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def upload_headers(_request); end
|
|
63
|
+
|
|
64
|
+
def file_headers(_request); end
|
|
65
|
+
|
|
66
|
+
def file_attachment(file, filename: nil)
|
|
67
|
+
return file if file.is_a?(Attachment) && filename.nil?
|
|
68
|
+
|
|
69
|
+
file = file.source if file.is_a?(Attachment)
|
|
70
|
+
Attachment.new(file, filename:, config:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def file_part(attachment, content_type: nil)
|
|
74
|
+
Faraday::Multipart::FilePart.new(file_part_source(attachment), content_type || file_content_type(attachment),
|
|
75
|
+
attachment.filename)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def file_content_type(attachment)
|
|
79
|
+
attachment.extension == 'jsonl' ? 'application/jsonl' : attachment.mime_type
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def file_part_source(attachment)
|
|
83
|
+
if attachment.path?
|
|
84
|
+
attachment.source.to_s
|
|
85
|
+
elsif attachment.io_like?
|
|
86
|
+
attachment.source.tap { |io| io.rewind if io.respond_to?(:rewind) }
|
|
87
|
+
else
|
|
88
|
+
StringIO.new(attachment.content)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def timestamp(value)
|
|
93
|
+
return if value.nil?
|
|
94
|
+
return Time.at(value) if value.is_a?(Numeric)
|
|
95
|
+
return Time.at(value.to_i) if value.to_s.match?(/\A\d+\z/)
|
|
96
|
+
|
|
97
|
+
Time.iso8601(value.to_s)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def uploaded_file(data, **attributes)
|
|
101
|
+
UploadedFile.new(**attributes, provider: @provider.slug, metadata: data)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def with_file_body(attachment, &)
|
|
105
|
+
if attachment.path?
|
|
106
|
+
File.open(attachment.source, 'rb', &)
|
|
107
|
+
else
|
|
108
|
+
body = attachment.io_like? ? attachment.source : StringIO.new(attachment.content)
|
|
109
|
+
body.rewind if body.respond_to?(:rewind)
|
|
110
|
+
yield body
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def file_size(attachment)
|
|
115
|
+
attachment.path? ? File.size(attachment.source) : attachment.content.bytesize
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|