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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The AWS Bedrock Converse API. Requests are SigV4-signed by the provider.
|
|
6
|
+
class Converse < Protocol
|
|
7
|
+
REGION_PREFIXES = %w[global us-gov us eu apac ap sa ca me af il au jp].freeze
|
|
8
|
+
|
|
9
|
+
include Converse::Chat
|
|
10
|
+
include Converse::Media
|
|
11
|
+
include Converse::Streaming
|
|
12
|
+
|
|
13
|
+
# Nova 2 models execute built-in tools server-side when the request
|
|
14
|
+
# names them as system tools.
|
|
15
|
+
SERVER_TOOL_ALIASES = {
|
|
16
|
+
web_search: lambda { |options|
|
|
17
|
+
{ tool: { systemTool: { name: 'nova_grounding' }.merge(Support::Utils.deep_symbolize_keys(options)) } }
|
|
18
|
+
}
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def server_tool_aliases
|
|
22
|
+
SERVER_TOOL_ALIASES
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Converse carries tools under toolConfig.tools rather than a
|
|
28
|
+
# top-level tools array.
|
|
29
|
+
def merge_server_tool_entries(payload, entries)
|
|
30
|
+
tool_config = payload[:toolConfig] ||= {}
|
|
31
|
+
tool_config[:tools] = Array(tool_config[:tools]) + entries
|
|
32
|
+
payload
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sync_response(payload, additional_headers = {})
|
|
36
|
+
response = signed_post(completion_url, payload, additional_headers)
|
|
37
|
+
parse_completion_response(response)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def post_count_tokens(payload)
|
|
41
|
+
signed_post(count_tokens_url, payload)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def signed_post(url, payload, additional_headers = {})
|
|
45
|
+
body = JSON.generate(payload)
|
|
46
|
+
|
|
47
|
+
@connection.post(url, payload, usage: @usage_tracker) do |req|
|
|
48
|
+
req.headers.merge!(@provider.sign_headers('POST', url, body))
|
|
49
|
+
req.headers.merge!(additional_headers) unless additional_headers.empty?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Deepgram
|
|
6
|
+
# Model catalog for Deepgram. GET v1/models splits the catalog into
|
|
7
|
+
# stt and tts groups, and names each entry twice: +name+ is the short
|
|
8
|
+
# name, such as the voice 'zeus', while +canonical_name+ is the id the
|
|
9
|
+
# listen and speak endpoints accept.
|
|
10
|
+
module Models
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def models_url
|
|
14
|
+
'v1/models'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_list_models_response(response, slug)
|
|
18
|
+
body = response.body || {}
|
|
19
|
+
|
|
20
|
+
%w[stt tts].flat_map do |group|
|
|
21
|
+
Array(body[group]).group_by { |data| data['canonical_name'] || data['name'] }
|
|
22
|
+
.map { |_, rows| build_model(merge_language_rows(rows), slug, group) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Deepgram lists a model once per language it was trained on, so
|
|
27
|
+
# nova-3-general arrives 146 times with a different language and
|
|
28
|
+
# version each time. One model answers to that id, so the rows
|
|
29
|
+
# collapse into it, carrying every language between them. Fields
|
|
30
|
+
# the rows disagree on describe a single language rather than the
|
|
31
|
+
# model, and are left out.
|
|
32
|
+
def merge_language_rows(rows)
|
|
33
|
+
merged = rows.first.merge('languages' => rows.flat_map { |row| Array(row['languages']) }.uniq.sort)
|
|
34
|
+
%w[version architecture].each do |key|
|
|
35
|
+
merged.delete(key) unless rows.map { |row| row[key] }.uniq.one?
|
|
36
|
+
end
|
|
37
|
+
merged
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_model(data, slug, group)
|
|
41
|
+
model_id = data['canonical_name'] || data['name']
|
|
42
|
+
transcription = group == 'stt' && !model_id.start_with?('flux-')
|
|
43
|
+
modalities = if group == 'stt'
|
|
44
|
+
{ input: ['audio'], output: ['text'] }
|
|
45
|
+
else
|
|
46
|
+
{ input: ['text'], output: ['audio'] }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Model.new(
|
|
50
|
+
id: model_id,
|
|
51
|
+
name: model_id,
|
|
52
|
+
provider: slug,
|
|
53
|
+
family: data['architecture'],
|
|
54
|
+
modalities: modalities,
|
|
55
|
+
capabilities: transcription ? ['transcription'] : [],
|
|
56
|
+
pricing: {},
|
|
57
|
+
metadata: build_metadata(data)
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_metadata(data)
|
|
62
|
+
metadata = {
|
|
63
|
+
architecture: data['architecture'],
|
|
64
|
+
languages: data['languages'],
|
|
65
|
+
version: data['version']
|
|
66
|
+
}
|
|
67
|
+
metadata[:voice] = data['name'] if data['name'] && data['name'] != data['canonical_name']
|
|
68
|
+
metadata.merge!(data['metadata'].transform_keys(&:to_sym)) if data['metadata'].is_a?(Hash)
|
|
69
|
+
metadata.compact
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Protocols
|
|
7
|
+
class Deepgram
|
|
8
|
+
# Speech dialect for the Deepgram text-to-speech API. Deepgram names
|
|
9
|
+
# the voice inside the model id, so aura-2-thalia-en is the Thalia
|
|
10
|
+
# voice of Aura 2: asking for a +voice:+ swaps that segment of the
|
|
11
|
+
# model id. The container is an encoding query value, the body carries
|
|
12
|
+
# only the text, and the response is raw audio bytes.
|
|
13
|
+
module Speech
|
|
14
|
+
DEFAULT_FORMAT = 'mp3'
|
|
15
|
+
|
|
16
|
+
# RubyLLM format names in Deepgram's encoding vocabulary. Deepgram
|
|
17
|
+
# wraps linear16 in a WAV header unless the container is turned off,
|
|
18
|
+
# and the compressed encodings carry their own.
|
|
19
|
+
OUTPUT_FORMATS = {
|
|
20
|
+
'aac' => { encoding: 'aac' },
|
|
21
|
+
'alaw' => { encoding: 'alaw', container: 'wav' },
|
|
22
|
+
'flac' => { encoding: 'flac' },
|
|
23
|
+
'mp3' => { encoding: 'mp3' },
|
|
24
|
+
'mulaw' => { encoding: 'mulaw', container: 'wav' },
|
|
25
|
+
'opus' => { encoding: 'opus' },
|
|
26
|
+
'pcm' => { encoding: 'linear16', container: 'none' },
|
|
27
|
+
'wav' => { encoding: 'linear16', container: 'wav' }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
def speak(input, model:, voice:, format:, provider_options: {}, &block)
|
|
31
|
+
track_usage(:speech) do
|
|
32
|
+
spoken_model = speech_model_for(model, voice)
|
|
33
|
+
payload = render_speech_payload(input, model: spoken_model, voice:, format:)
|
|
34
|
+
url = speech_url(model: spoken_model, format:, provider_options:)
|
|
35
|
+
if block
|
|
36
|
+
next stream_speech_response(url, payload, model: spoken_model, voice: voice_for(spoken_model),
|
|
37
|
+
format:, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
response = @connection.post url, payload, usage: @usage_tracker
|
|
41
|
+
parse_speech_response(response, model: spoken_model, voice: voice_for(spoken_model), format:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def speech_url(model:, format: nil, provider_options: {})
|
|
46
|
+
"v1/speak?#{URI.encode_www_form(speech_params(model:, format:, provider_options:))}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def speech_params(model:, format: nil, provider_options: {})
|
|
50
|
+
{ model: model }.merge(OUTPUT_FORMATS.fetch(format_for(format), encoding: format_for(format)))
|
|
51
|
+
.merge(provider_options).compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
55
|
+
def render_speech_payload(input, model:, voice: nil, format: nil, provider_options: {})
|
|
56
|
+
{ text: input }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_speech_response(response, model:, voice:, format:)
|
|
60
|
+
RubyLLM::Speech.new(
|
|
61
|
+
data: response.body,
|
|
62
|
+
model: model,
|
|
63
|
+
voice: voice,
|
|
64
|
+
format: format_for(format)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Puts +voice+ into the voice segment of +model+, so Aura 2 with
|
|
69
|
+
# voice 'zeus' is aura-2-zeus-en. A voice that already names a whole
|
|
70
|
+
# model is used as it stands.
|
|
71
|
+
def speech_model_for(model, voice)
|
|
72
|
+
return model unless voice
|
|
73
|
+
|
|
74
|
+
segments = model.to_s.split('-')
|
|
75
|
+
return voice if voice.include?('-') || segments.size < 3
|
|
76
|
+
|
|
77
|
+
(segments[0..-3] + [voice, segments[-1]]).join('-')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The voice segment of a Deepgram model id, so aura-2-thalia-en
|
|
81
|
+
# speaks as 'thalia'.
|
|
82
|
+
def voice_for(model)
|
|
83
|
+
segments = model.to_s.split('-')
|
|
84
|
+
segments[-2] if segments.size >= 3
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def format_for(format)
|
|
88
|
+
(format || DEFAULT_FORMAT).to_s
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Deepgram
|
|
6
|
+
module StreamingTranscription # :nodoc: all
|
|
7
|
+
def stream_live_transcription(audio_file, model:, language:, speaker_names:, provider_options:, prompt:, &block)
|
|
8
|
+
attachment = Attachment.wrap(audio_file, config: @config)
|
|
9
|
+
raise ArgumentError, 'Transcription requires exactly one audio file' unless attachment.one?
|
|
10
|
+
|
|
11
|
+
track_usage(:transcription) do
|
|
12
|
+
url = streaming_transcription_url(model:, language:, speaker_names:, provider_options:, prompt:)
|
|
13
|
+
segments = []
|
|
14
|
+
metadata = nil
|
|
15
|
+
@usage_tracker.start
|
|
16
|
+
Transport::WebsocketConnection.open(url, headers: @provider.headers, config: @config) do |socket|
|
|
17
|
+
write = ->(connection) { send_transcription_audio(connection, attachment.first.content) }
|
|
18
|
+
socket.each_message(write:) do |message|
|
|
19
|
+
event = JSON.parse(message)
|
|
20
|
+
case event['type']
|
|
21
|
+
when 'Results'
|
|
22
|
+
process_transcription_result(event, segments, &block)
|
|
23
|
+
when 'Metadata'
|
|
24
|
+
metadata = event
|
|
25
|
+
when 'Error'
|
|
26
|
+
raise Error, event['description'] || event['message'] || 'Deepgram transcription failed'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
raise Error, 'Deepgram transcription ended before its completion metadata' unless metadata
|
|
31
|
+
|
|
32
|
+
result = build_live_transcription(segments, metadata, model:, language:)
|
|
33
|
+
block.call(TranscriptionChunk.new(type: TranscriptionChunk::DONE, text: result.text, raw: metadata))
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def streaming_transcription_url(model:, language:, speaker_names:, provider_options:, prompt:)
|
|
39
|
+
params = { model:, language:, smart_format: true, interim_results: true }
|
|
40
|
+
params[:diarize_model] = DIARIZE_MODEL if speaker_names
|
|
41
|
+
params[:keyterm] = Array(prompt) if prompt
|
|
42
|
+
params.merge!(provider_options)
|
|
43
|
+
params = params.compact.flat_map { |key, value| Array(value).map { |item| [key, item] } }
|
|
44
|
+
uri = URI.join("#{@provider.api_base.sub(%r{/+\z}, '')}/", "v1/listen?#{URI.encode_www_form(params)}")
|
|
45
|
+
uri.scheme = uri.scheme == 'https' ? 'wss' : 'ws'
|
|
46
|
+
uri.to_s
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def send_transcription_audio(socket, audio)
|
|
50
|
+
offset = 0
|
|
51
|
+
while offset < audio.bytesize
|
|
52
|
+
socket.send_binary(audio.byteslice(offset, 16_384))
|
|
53
|
+
offset += 16_384
|
|
54
|
+
end
|
|
55
|
+
socket.send_text(JSON.generate(type: 'CloseStream'))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def process_transcription_result(event, segments)
|
|
59
|
+
alternative = event.dig('channel', 'alternatives', 0) || {}
|
|
60
|
+
text = alternative['transcript'].to_s
|
|
61
|
+
return if text.empty?
|
|
62
|
+
|
|
63
|
+
unless event['is_final']
|
|
64
|
+
yield TranscriptionChunk.new(type: TranscriptionChunk::PARTIAL, text:, raw: event)
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
delta = segments.empty? ? text : " #{text}"
|
|
69
|
+
segment = {
|
|
70
|
+
'text' => text,
|
|
71
|
+
'start' => event['start'],
|
|
72
|
+
'end' => event['start'].to_f + event['duration'].to_f,
|
|
73
|
+
'channel' => event.dig('channel_index', 0),
|
|
74
|
+
'words' => alternative['words']
|
|
75
|
+
}.compact
|
|
76
|
+
segments << segment
|
|
77
|
+
yield TranscriptionChunk.new(type: TranscriptionChunk::SEGMENT, delta:, segment:, raw: event)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def build_live_transcription(segments, metadata, model:, language:)
|
|
81
|
+
RubyLLM::Transcription.new(
|
|
82
|
+
text: segments.map { |segment| segment.fetch('text') }.join(' '), model:, language:,
|
|
83
|
+
duration: metadata['duration'], segments:, words: segments.flat_map { |segment| segment['words'] || [] }
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Protocols
|
|
7
|
+
class Deepgram
|
|
8
|
+
# Transcription dialect for the Deepgram speech-to-text API. Every
|
|
9
|
+
# option is a query parameter on v1/listen, and the body is either the
|
|
10
|
+
# audio bytes or a JSON pointer to a remote url. Giving speaker names
|
|
11
|
+
# turns on diarization, which labels each word and utterance with a
|
|
12
|
+
# numeric speaker index.
|
|
13
|
+
module Transcription
|
|
14
|
+
def render_transcription_options(timestamps:, **)
|
|
15
|
+
return {} if timestamps.nil? || timestamps == :word
|
|
16
|
+
|
|
17
|
+
raise ArgumentError, 'Deepgram transcription timestamps must be word'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Turned on by default so the transcript reads like the transcripts
|
|
21
|
+
# every other provider returns. Smart formatting punctuates and
|
|
22
|
+
# formats numbers, dates, and currency; utterances split the
|
|
23
|
+
# transcript into timed segments.
|
|
24
|
+
DEFAULT_PARAMS = { smart_format: true, utterances: true }.freeze
|
|
25
|
+
|
|
26
|
+
# The batch diarizer to run. 'latest' tracks Deepgram's current
|
|
27
|
+
# generally available diarizer, which the boolean diarize parameter
|
|
28
|
+
# it deprecates does not.
|
|
29
|
+
DIARIZE_MODEL = 'latest'
|
|
30
|
+
|
|
31
|
+
def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
|
|
32
|
+
speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &block)
|
|
33
|
+
if block
|
|
34
|
+
if format || speaker_references || temperature
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
'Deepgram streaming transcription does not accept format, speaker_references or temperature'
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return stream_live_transcription(audio_file, model:, language:, speaker_names:, provider_options:, prompt:,
|
|
40
|
+
&block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
track_usage(:transcription) do
|
|
44
|
+
attachment = Attachment.new(audio_file, config: @config)
|
|
45
|
+
url = transcription_url(model:, language:, speaker_names:, provider_options:)
|
|
46
|
+
payload = render_transcription_payload(attachment)
|
|
47
|
+
response = post_transcription(url, payload, attachment)
|
|
48
|
+
parse_transcription_response(response, model:)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def transcription_url(model:, language: nil, speaker_names: nil, provider_options: {})
|
|
53
|
+
"v1/listen?#{URI.encode_www_form(transcription_params(model:, language:, speaker_names:,
|
|
54
|
+
provider_options:))}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def transcription_params(model:, language: nil, speaker_names: nil, provider_options: {})
|
|
58
|
+
params = { model: model, language: language }.merge(DEFAULT_PARAMS)
|
|
59
|
+
params[:diarize_model] = DIARIZE_MODEL if speaker_names
|
|
60
|
+
params.merge(provider_options).compact
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Deepgram fetches remote audio itself, so a url attachment is sent
|
|
64
|
+
# as a JSON pointer and everything else is uploaded as raw bytes.
|
|
65
|
+
def render_transcription_payload(attachment)
|
|
66
|
+
return { url: attachment.source.to_s } if attachment.url?
|
|
67
|
+
|
|
68
|
+
attachment.content
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_transcription_response(response, model:)
|
|
72
|
+
data = response.body || {}
|
|
73
|
+
channel = data.dig('results', 'channels', 0) || {}
|
|
74
|
+
alternative = channel.dig('alternatives', 0) || {}
|
|
75
|
+
|
|
76
|
+
RubyLLM::Transcription.new(
|
|
77
|
+
text: alternative['transcript'],
|
|
78
|
+
model: model,
|
|
79
|
+
language: channel['detected_language'],
|
|
80
|
+
duration: data.dig('metadata', 'duration'),
|
|
81
|
+
segments: data.dig('results', 'utterances'),
|
|
82
|
+
words: alternative['words']
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def post_transcription(url, payload, attachment)
|
|
89
|
+
@connection.post(url, payload, usage: @usage_tracker) do |request|
|
|
90
|
+
request.headers['Content-Type'] = attachment.mime_type || 'application/octet-stream' unless attachment.url?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The Deepgram API: speech to text on v1/listen, text to speech on
|
|
6
|
+
# v1/speak, and the model catalog behind them. Deepgram has no chat,
|
|
7
|
+
# embedding, or image endpoints, so those seams are left unimplemented.
|
|
8
|
+
#
|
|
9
|
+
# Deepgram carries its request options in the query string rather than
|
|
10
|
+
# the body, so +provider_options:+ joins the query on both audio
|
|
11
|
+
# operations.
|
|
12
|
+
class Deepgram < Protocol
|
|
13
|
+
include Deepgram::Models
|
|
14
|
+
include Deepgram::Speech
|
|
15
|
+
include Deepgram::Transcription
|
|
16
|
+
include Deepgram::StreamingTranscription
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
module DeepSeek
|
|
6
|
+
# DeepSeek stores image files for reuse by vision models.
|
|
7
|
+
class Files < Protocols::OpenAI::Files
|
|
8
|
+
IMAGE_TYPES = %w[image/jpeg image/png image/gif image/webp].freeze
|
|
9
|
+
MAX_FILE_SIZE = 64 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
def download(_file_id)
|
|
12
|
+
raise Error, 'DeepSeek does not support downloading uploaded files'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def render_upload_payload(attachment, purpose: nil, **options)
|
|
18
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless IMAGE_TYPES.include?(attachment.mime_type)
|
|
19
|
+
raise ArgumentError, 'DeepSeek image uploads cannot exceed 64 MiB' if file_size(attachment) > MAX_FILE_SIZE
|
|
20
|
+
raise ArgumentError, 'DeepSeek file uploads require purpose: user_data' if purpose && purpose != 'user_data'
|
|
21
|
+
|
|
22
|
+
super(attachment, purpose: 'user_data', **options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def uploaded_file(data, **attributes)
|
|
26
|
+
super(data, **attributes, downloadable: false)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
# Workspace media assets used by the Image & Video API.
|
|
7
|
+
class Assets < Protocols::Files
|
|
8
|
+
def download(file_id)
|
|
9
|
+
file = find(file_id)
|
|
10
|
+
url = file.metadata['content_url']
|
|
11
|
+
raise Error, 'ElevenLabs asset is still processing; retrieve it again before downloading' unless url
|
|
12
|
+
|
|
13
|
+
Transport::Connection.basic(@config).get(url).body
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def files_url
|
|
19
|
+
'v1/assets'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_upload_payload(attachment, purpose: nil, expires_in: nil, name: nil)
|
|
23
|
+
raise ArgumentError, 'ElevenLabs assets do not accept purpose or expires_in' if purpose || expires_in
|
|
24
|
+
|
|
25
|
+
{ asset: file_part(attachment), name: name || attachment.filename }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def parse_file_response(data)
|
|
29
|
+
uploaded_file(data, id: data.fetch('asset_id'), filename: data['name'], mime_type: data['mime_type'],
|
|
30
|
+
created_at: timestamp(data['created_at_unix']), downloadable: true,
|
|
31
|
+
status: data['content_url'] ? 'ready' : 'processing')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
class Flows
|
|
7
|
+
module Images # :nodoc: all
|
|
8
|
+
MASK_MODELS = %w[gpt-image-1 gpt-image-1.5 gpt-image-2].freeze
|
|
9
|
+
|
|
10
|
+
def images_url(**)
|
|
11
|
+
'v1/flows/image'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def post_image(payload, **)
|
|
15
|
+
@connection.post images_url, payload, usage: @usage_tracker, idempotent: false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render_image_payload(prompt, model:, size:, with: nil, mask: nil, provider_options: {}, **)
|
|
19
|
+
images = Attachment.wrap(with, config: @config)
|
|
20
|
+
payload = { model_id: model, prompt: }
|
|
21
|
+
payload[:images] = images.map { |image| render_media_reference(image) } if images.any?
|
|
22
|
+
payload[:aspect_ratio] = image_aspect_ratio(size) if size && size != 'auto'
|
|
23
|
+
if mask
|
|
24
|
+
raise ArgumentError, 'ElevenLabs image masks require a GPT Image model' unless MASK_MODELS.include?(model)
|
|
25
|
+
|
|
26
|
+
payload[:mask] = render_media_reference(Attachment.wrap(mask, config: @config).first)
|
|
27
|
+
end
|
|
28
|
+
Support::Utils.deep_merge(payload, provider_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_image_response(response, model:)
|
|
32
|
+
id = response.body.fetch('id')
|
|
33
|
+
body = wait_for_image(id)
|
|
34
|
+
Image.new(url: body.fetch('content_url'), mime_type: body.fetch('content_mime_type'), model:)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def wait_for_image(id)
|
|
38
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config.request_timeout
|
|
39
|
+
loop do
|
|
40
|
+
body = @connection.get("#{images_url}/#{id}").body
|
|
41
|
+
state = parse_generation_status(body)
|
|
42
|
+
return body if state[:status] == :completed
|
|
43
|
+
raise Error, "ElevenLabs image generation failed: #{state[:error]}" if state[:status] == :failed
|
|
44
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
45
|
+
raise Error, "ElevenLabs image generation timed out: #{id}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sleep 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_paint_inputs!(with:, mask:)
|
|
53
|
+
images = Attachment.wrap(with, config: @config)
|
|
54
|
+
raise ArgumentError, 'An image mask requires a source image' if mask && images.empty?
|
|
55
|
+
|
|
56
|
+
images += Attachment.wrap(mask, config: @config) if mask
|
|
57
|
+
images.each do |image|
|
|
58
|
+
raise UnsupportedAttachmentError, image.mime_type unless image.image?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def image_aspect_ratio(size)
|
|
63
|
+
match = size.to_s.match(/\A([1-9]\d*)x([1-9]\d*)\z/)
|
|
64
|
+
raise ArgumentError, 'size must be widthxheight or auto' unless match
|
|
65
|
+
|
|
66
|
+
width, height = match.captures.map(&:to_i)
|
|
67
|
+
divisor = width.gcd(height)
|
|
68
|
+
"#{width / divisor}:#{height / divisor}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ElevenLabs
|
|
6
|
+
class Flows
|
|
7
|
+
module Media # :nodoc: all
|
|
8
|
+
def render_media_reference(attachment)
|
|
9
|
+
unless attachment.image? || attachment.audio? || attachment.video?
|
|
10
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
if attachment.provider_file?
|
|
14
|
+
unless attachment.source.provider.to_s == @provider.slug
|
|
15
|
+
raise ArgumentError, 'ElevenLabs media references require an asset from the same provider'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
{ type: 'asset', asset_id: attachment.provider_file_id }
|
|
19
|
+
else
|
|
20
|
+
{ type: 'inline_base64', content_base64: attachment.encoded, mime_type: attachment.mime_type }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_generation_status(body)
|
|
25
|
+
case body['status']
|
|
26
|
+
when 'pending', 'generating'
|
|
27
|
+
{ status: :pending, raw: body }
|
|
28
|
+
when 'completed'
|
|
29
|
+
raise Error, 'ElevenLabs completed a generation without an output URL' unless body['content_url']
|
|
30
|
+
|
|
31
|
+
{ status: :completed, raw: body }
|
|
32
|
+
when 'failed'
|
|
33
|
+
{ status: :failed, raw: body, error: body['error_message'] || body['failure_reason'] }
|
|
34
|
+
else
|
|
35
|
+
raise Error, "ElevenLabs returned an unknown generation status: #{body['status'].inspect}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|