ruby_llm 1.16.0 → 2.0.0.rc4
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 +108 -44
- 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/legacy_content_sql.rb +34 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +341 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +171 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +197 -0
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +174 -0
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +468 -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 +247 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +662 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +251 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +188 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +359 -0
- data/lib/ruby_llm/accounting/usage.rb +254 -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 +1066 -151
- data/lib/ruby_llm/aliases.json +291 -101
- data/lib/ruby_llm/attachment.rb +192 -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 +1131 -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 +242 -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 +76038 -34173
- 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 +543 -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 +110 -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 +484 -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 +63 -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 +59 -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 +694 -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 +91 -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 +53 -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 +466 -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 +67 -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 +51 -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 +126 -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 +344 -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
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
module
|
|
5
|
-
class
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
6
|
# Handles formatting of media content (images, audio) for OpenAI APIs
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def format_content(content, document_attachments: :pdf, image_attachments: true,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
end
|
|
15
|
-
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
|
16
|
-
return content unless content.is_a?(Content)
|
|
17
|
-
|
|
18
|
-
parts = []
|
|
19
|
-
parts << format_text(content.text) if content.text
|
|
20
|
-
|
|
21
|
-
content.attachments.each do |attachment|
|
|
22
|
-
parts << format_attachment(
|
|
10
|
+
def format_content(content, attachments = [], document_attachments: :pdf, image_attachments: true,
|
|
11
|
+
audio_attachments: true)
|
|
12
|
+
format_parts(content, attachments) do |attachment|
|
|
13
|
+
format_attachment(
|
|
23
14
|
attachment,
|
|
24
15
|
document_attachments:,
|
|
25
16
|
image_attachments:,
|
|
26
17
|
audio_attachments:
|
|
27
18
|
)
|
|
28
19
|
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Shared preamble and attachment loop for OpenAI-compatible providers.
|
|
23
|
+
# The block formats a single attachment in the provider's dialect.
|
|
24
|
+
def format_parts(content, attachments = [])
|
|
25
|
+
return content if attachments.empty?
|
|
26
|
+
|
|
27
|
+
parts = []
|
|
28
|
+
parts << format_text(content) if content
|
|
29
|
+
|
|
30
|
+
attachments.each do |attachment|
|
|
31
|
+
parts << yield(attachment)
|
|
32
|
+
end
|
|
29
33
|
|
|
30
34
|
parts
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
def format_attachment(attachment, document_attachments:, image_attachments:, audio_attachments:)
|
|
38
|
+
return format_provider_file(attachment, document_attachments:) if attachment.provider_file?
|
|
39
|
+
|
|
34
40
|
case attachment.type
|
|
35
41
|
when :image
|
|
36
42
|
raise UnsupportedAttachmentError, attachment.mime_type unless image_attachments
|
|
@@ -68,8 +74,15 @@ module RubyLLM
|
|
|
68
74
|
}
|
|
69
75
|
end
|
|
70
76
|
|
|
71
|
-
def
|
|
72
|
-
|
|
77
|
+
def format_provider_file(file, document_attachments:)
|
|
78
|
+
raise UnsupportedAttachmentError, file.mime_type if document_attachments == :none
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
type: 'file',
|
|
82
|
+
file: {
|
|
83
|
+
file_id: file.provider_file_id
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
end
|
|
74
87
|
|
|
75
88
|
def format_text_file(text_file)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# Models methods of the OpenAI API integration
|
|
7
|
+
module Models
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def models_url
|
|
11
|
+
'models'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_list_models_response(response, slug)
|
|
15
|
+
Array(response.body['data']).map do |model_data|
|
|
16
|
+
model_id = model_data['id']
|
|
17
|
+
|
|
18
|
+
Model.new(
|
|
19
|
+
id: model_id,
|
|
20
|
+
name: model_id,
|
|
21
|
+
provider: slug,
|
|
22
|
+
created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
|
|
23
|
+
metadata: model_metadata(model_data)
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def model_metadata(model_data)
|
|
29
|
+
metadata = {
|
|
30
|
+
object: model_data['object'],
|
|
31
|
+
owned_by: model_data['owned_by']
|
|
32
|
+
}
|
|
33
|
+
metadata[:shutdown_date] = model_data['shutdown_date'] if model_data['shutdown_date']
|
|
34
|
+
metadata
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# Moderation methods of the OpenAI API integration
|
|
7
|
+
module Moderation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def moderation_url
|
|
11
|
+
'moderations'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_moderation_payload(input, model:, with: [], provider_options: {})
|
|
15
|
+
attachments = Attachment.wrap(with)
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
model: model,
|
|
19
|
+
input: moderation_input(input, attachments)
|
|
20
|
+
}.merge(provider_options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_moderation_response(response, model:)
|
|
24
|
+
data = response.body
|
|
25
|
+
raise Error.new(data.dig('error', 'message'), response:) if data.dig('error', 'message')
|
|
26
|
+
|
|
27
|
+
RubyLLM::Moderation.new(
|
|
28
|
+
id: data['id'],
|
|
29
|
+
model: model,
|
|
30
|
+
results: Array(data['results']).map { |result| RubyLLM::Moderation::Result.from_h(result) },
|
|
31
|
+
raw: data
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def moderation_input(input, attachments)
|
|
36
|
+
return input if attachments.empty?
|
|
37
|
+
|
|
38
|
+
parts = []
|
|
39
|
+
parts << Media.format_text(input) if input
|
|
40
|
+
parts.concat(attachments.map { |attachment| format_moderation_attachment(attachment) })
|
|
41
|
+
parts
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def format_moderation_attachment(attachment)
|
|
45
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
46
|
+
|
|
47
|
+
Media.format_image(attachment)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# The Jina-style rerank endpoint several OpenAI-compatible providers
|
|
7
|
+
# serve: OpenRouter at /api/v1/rerank and GPUStack at /v1/rerank,
|
|
8
|
+
# with identical request and response shapes.
|
|
9
|
+
module Rerank
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def rerank_url
|
|
13
|
+
'rerank'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
|
|
17
|
+
{
|
|
18
|
+
model: model,
|
|
19
|
+
query: query,
|
|
20
|
+
documents: documents,
|
|
21
|
+
top_n: top_n
|
|
22
|
+
}.compact.merge(provider_options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse_rerank_response(response, model:, documents: [])
|
|
26
|
+
data = response.body
|
|
27
|
+
usage = data['usage'] || {}
|
|
28
|
+
|
|
29
|
+
RubyLLM::Rerank.new(
|
|
30
|
+
results: parse_rerank_results(data, documents),
|
|
31
|
+
model: data['model'] || model,
|
|
32
|
+
raw: data,
|
|
33
|
+
input_tokens: usage['total_tokens'],
|
|
34
|
+
reported_cost: usage['cost']
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_rerank_results(data, documents = [])
|
|
39
|
+
Array(data['results']).map do |result|
|
|
40
|
+
index = result['index']
|
|
41
|
+
unless valid_rerank_index?(index, documents)
|
|
42
|
+
raise Error, 'Rerank endpoint returned an invalid document index'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
RubyLLM::Rerank::Result.new(
|
|
46
|
+
index: index,
|
|
47
|
+
document: rerank_document(result['document']) || documents[index],
|
|
48
|
+
score: result['relevance_score']
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rerank_document(document)
|
|
54
|
+
document.is_a?(Hash) ? document['text'] : document
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def valid_rerank_index?(index, documents)
|
|
58
|
+
index.is_a?(Integer) && index.between?(0, documents.length - 1)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# Speech generation methods for the OpenAI API integration
|
|
7
|
+
module Speech
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def speech_url(model:) # rubocop:disable Lint/UnusedMethodArgument
|
|
11
|
+
'audio/speech'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stream_speech(payload, model:, voice:, format:, &)
|
|
15
|
+
stream_speech_response(speech_url(model:), payload, model:, voice:, format:, &)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render_speech_payload(input, model:, voice:, format:, provider_options: {})
|
|
19
|
+
{
|
|
20
|
+
model: model,
|
|
21
|
+
input: input,
|
|
22
|
+
voice: voice || 'alloy',
|
|
23
|
+
response_format: format
|
|
24
|
+
}.compact.merge(provider_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_speech_response(response, model:, voice:, format:)
|
|
28
|
+
resolved_format = (format || 'mp3').to_s
|
|
29
|
+
|
|
30
|
+
RubyLLM::Speech.new(
|
|
31
|
+
data: response.body,
|
|
32
|
+
model: model,
|
|
33
|
+
voice: voice || 'alloy',
|
|
34
|
+
format: resolved_format
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Protocols
|
|
7
|
+
class ChatCompletions
|
|
8
|
+
# Streaming methods of the OpenAI API integration
|
|
9
|
+
module Streaming
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def stream_url
|
|
13
|
+
completion_url
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_chunk(data)
|
|
17
|
+
usage = data['usage'] || {}
|
|
18
|
+
delta = data.dig('choices', 0, 'delta') || {}
|
|
19
|
+
content_source = delta['content'] || data.dig('choices', 0, 'message', 'content')
|
|
20
|
+
content, thinking_from_blocks = extract_content_and_thinking(content_source)
|
|
21
|
+
|
|
22
|
+
Chunk.new(
|
|
23
|
+
role: :assistant,
|
|
24
|
+
model: data['model'],
|
|
25
|
+
content: content,
|
|
26
|
+
citations: extract_chunk_citations(delta, data),
|
|
27
|
+
thinking: Thinking.build(
|
|
28
|
+
text: thinking_from_blocks || delta['reasoning_content'] || delta['reasoning'],
|
|
29
|
+
signature: delta['reasoning_signature']
|
|
30
|
+
),
|
|
31
|
+
tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false, stream_keys: true),
|
|
32
|
+
input_tokens: input_tokens(usage),
|
|
33
|
+
output_tokens: output_tokens(usage),
|
|
34
|
+
cache_read_tokens: cache_read_tokens(usage),
|
|
35
|
+
cache_write_tokens: cache_write_tokens(usage),
|
|
36
|
+
thinking_tokens: thinking_tokens(usage),
|
|
37
|
+
server_tool_use: server_tool_use(usage),
|
|
38
|
+
reported_cost: reported_cost(usage),
|
|
39
|
+
finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason'))
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def extract_chunk_citations(delta, data)
|
|
44
|
+
annotations = parse_annotations(delta['annotations'], nil)
|
|
45
|
+
return annotations if annotations.any?
|
|
46
|
+
|
|
47
|
+
parse_root_citations(data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse_streaming_error(data)
|
|
51
|
+
error_data = JSON.parse(data)
|
|
52
|
+
return [nil, error_data.to_s] unless error_data.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
error = error_data['error']
|
|
55
|
+
return [nil, error.to_s] unless error.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
case error['type']
|
|
58
|
+
when 'server_error'
|
|
59
|
+
[500, error['message']]
|
|
60
|
+
when 'rate_limit_exceeded', 'insufficient_quota'
|
|
61
|
+
[429, error['message']]
|
|
62
|
+
else
|
|
63
|
+
[400, error['message']]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require 'json'
|
|
4
4
|
|
|
5
5
|
module RubyLLM
|
|
6
|
-
module
|
|
7
|
-
class
|
|
6
|
+
module Protocols
|
|
7
|
+
class ChatCompletions
|
|
8
8
|
# Tools methods of the OpenAI API integration
|
|
9
9
|
module Tools
|
|
10
10
|
module_function
|
|
@@ -18,8 +18,8 @@ module RubyLLM
|
|
|
18
18
|
}.freeze
|
|
19
19
|
|
|
20
20
|
def parameters_schema_for(tool)
|
|
21
|
-
tool.
|
|
22
|
-
schema_from_parameters(tool.
|
|
21
|
+
tool.parameters_schema ||
|
|
22
|
+
schema_from_parameters(tool.declared_parameters)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def schema_from_parameters(parameters)
|
|
@@ -39,16 +39,9 @@ module RubyLLM
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
return definition if tool.
|
|
42
|
+
return definition if tool.provider_options.empty?
|
|
43
43
|
|
|
44
|
-
RubyLLM::Utils.deep_merge(definition, tool.
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def param_schema(param)
|
|
48
|
-
{
|
|
49
|
-
type: param.type,
|
|
50
|
-
description: param.description
|
|
51
|
-
}.compact
|
|
44
|
+
RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
|
|
52
45
|
end
|
|
53
46
|
|
|
54
47
|
def format_tool_calls(tool_calls)
|
|
@@ -72,7 +65,7 @@ module RubyLLM
|
|
|
72
65
|
end
|
|
73
66
|
end
|
|
74
67
|
|
|
75
|
-
def parse_tool_call_arguments(tool_call)
|
|
68
|
+
def parse_tool_call_arguments(tool_call, response: nil, finish_reason: nil)
|
|
76
69
|
arguments = tool_call.dig('function', 'arguments')
|
|
77
70
|
|
|
78
71
|
if arguments.nil? || arguments.empty?
|
|
@@ -80,19 +73,24 @@ module RubyLLM
|
|
|
80
73
|
else
|
|
81
74
|
JSON.parse(arguments)
|
|
82
75
|
end
|
|
76
|
+
rescue JSON::ParserError => e
|
|
77
|
+
raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
|
|
83
78
|
end
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
# Streaming deltas key by index so id-less argument fragments find
|
|
81
|
+
# their call even when a delta batches or interleaves several calls;
|
|
82
|
+
# complete responses key by id, which tool results join on.
|
|
83
|
+
def parse_tool_calls(tool_calls, parse_arguments: true, response: nil, finish_reason: nil, stream_keys: false)
|
|
86
84
|
return nil unless tool_calls&.any?
|
|
87
85
|
|
|
88
86
|
tool_calls.to_h do |tc|
|
|
89
87
|
[
|
|
90
|
-
tc['id'],
|
|
88
|
+
stream_keys ? tc['index'] || tc['id'] : tc['id'],
|
|
91
89
|
ToolCall.new(
|
|
92
90
|
id: tc['id'],
|
|
93
91
|
name: tc.dig('function', 'name'),
|
|
94
92
|
arguments: if parse_arguments
|
|
95
|
-
parse_tool_call_arguments(tc)
|
|
93
|
+
parse_tool_call_arguments(tc, response: response, finish_reason: finish_reason)
|
|
96
94
|
else
|
|
97
95
|
tc.dig('function', 'arguments')
|
|
98
96
|
end,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# Audio transcription methods for the OpenAI API integration
|
|
7
|
+
module Transcription
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def transcription_url
|
|
11
|
+
'audio/transcriptions'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_transcription_options(timestamps:, format:, streaming:)
|
|
15
|
+
return {} if timestamps.nil?
|
|
16
|
+
|
|
17
|
+
values = Array(timestamps).map(&:to_s)
|
|
18
|
+
unless values.any? && (values - %w[word segment]).empty?
|
|
19
|
+
raise ArgumentError, 'Transcription timestamps must be word or segment'
|
|
20
|
+
end
|
|
21
|
+
if streaming || (format && format != 'verbose_json')
|
|
22
|
+
raise ArgumentError, 'Transcription timestamps require a non-streaming verbose_json response'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
{ response_format: 'verbose_json', timestamp_granularities: values }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
|
|
29
|
+
speaker_references: nil, provider_options: {}, prompt: nil,
|
|
30
|
+
temperature: nil)
|
|
31
|
+
{
|
|
32
|
+
model: model,
|
|
33
|
+
file: file_part,
|
|
34
|
+
language: language,
|
|
35
|
+
response_format: format || default_response_format(model),
|
|
36
|
+
prompt: prompt,
|
|
37
|
+
temperature: temperature,
|
|
38
|
+
known_speaker_names: speaker_names,
|
|
39
|
+
known_speaker_references: encode_speaker_references(speaker_references)
|
|
40
|
+
}.compact.merge(provider_options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def encode_speaker_references(references)
|
|
44
|
+
return nil unless references
|
|
45
|
+
|
|
46
|
+
references.map do |ref|
|
|
47
|
+
Attachment.new(ref, config: @config).for_llm
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def reported_cost(_usage)
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Diarization models return plain text with no segments unless the
|
|
56
|
+
# response format asks for them.
|
|
57
|
+
def default_response_format(model)
|
|
58
|
+
'diarized_json' if model.include?('diarize')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# OpenAI streams transcriptions as server-sent events carrying text
|
|
62
|
+
# deltas, completed segments on diarization models, and a final
|
|
63
|
+
# event with the whole transcript and its usage.
|
|
64
|
+
def stream_transcription(payload, model:, &block)
|
|
65
|
+
chunks = []
|
|
66
|
+
|
|
67
|
+
stream_events(transcription_url, payload.merge(stream: 'true')) do |data|
|
|
68
|
+
chunk = build_transcription_chunk(data)
|
|
69
|
+
chunks << chunk
|
|
70
|
+
block.call chunk
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
build_streamed_transcription(chunks, model: model)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_transcription_chunk(data)
|
|
77
|
+
type = data['type']
|
|
78
|
+
|
|
79
|
+
RubyLLM::TranscriptionChunk.new(
|
|
80
|
+
type: type,
|
|
81
|
+
delta: data['delta'],
|
|
82
|
+
text: (data['text'] if type == RubyLLM::TranscriptionChunk::DONE),
|
|
83
|
+
segment: (data.except('type') if type == RubyLLM::TranscriptionChunk::SEGMENT),
|
|
84
|
+
raw: data
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_streamed_transcription(chunks, model:)
|
|
89
|
+
final = chunks.reverse.find(&:done?)
|
|
90
|
+
data = final&.raw || {}
|
|
91
|
+
usage = data['usage'] || {}
|
|
92
|
+
|
|
93
|
+
RubyLLM::Transcription.new(
|
|
94
|
+
text: final&.text || streamed_transcript_text(chunks),
|
|
95
|
+
model: model,
|
|
96
|
+
language: data['language'],
|
|
97
|
+
duration: transcription_duration(usage),
|
|
98
|
+
segments: streamed_transcription_segments(chunks, data),
|
|
99
|
+
reported_cost: reported_cost(usage),
|
|
100
|
+
**transcription_tokens(usage)
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Diarization models stream segments instead of deltas, so the
|
|
105
|
+
# transcript is rebuilt from whichever the provider sent.
|
|
106
|
+
def streamed_transcript_text(chunks)
|
|
107
|
+
deltas = chunks.filter_map(&:delta)
|
|
108
|
+
return deltas.join if deltas.any?
|
|
109
|
+
|
|
110
|
+
chunks.filter_map { |chunk| chunk.segment&.fetch('text', nil) }.join(' ')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def streamed_transcription_segments(chunks, data)
|
|
114
|
+
segments = data['segments'] || chunks.filter_map(&:segment)
|
|
115
|
+
segments.empty? ? nil : segments
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_transcription_response(response, model:)
|
|
119
|
+
data = response.body
|
|
120
|
+
|
|
121
|
+
return RubyLLM::Transcription.new(text: data, model: model) if data.is_a?(String)
|
|
122
|
+
|
|
123
|
+
usage = data['usage'] || {}
|
|
124
|
+
|
|
125
|
+
RubyLLM::Transcription.new(
|
|
126
|
+
text: data['text'],
|
|
127
|
+
model: model,
|
|
128
|
+
language: data['language'],
|
|
129
|
+
duration: data['duration'] || transcription_duration(usage),
|
|
130
|
+
segments: data['segments'],
|
|
131
|
+
words: data['words'],
|
|
132
|
+
reported_cost: reported_cost(usage),
|
|
133
|
+
**transcription_tokens(usage)
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def transcription_tokens(usage)
|
|
138
|
+
{
|
|
139
|
+
input_tokens: usage['input_tokens'] || usage['prompt_tokens'],
|
|
140
|
+
output_tokens: usage['output_tokens'] || usage['completion_tokens']
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def transcription_duration(usage)
|
|
145
|
+
usage['seconds']
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The OpenAI Chat Completions API — the lingua franca of LLM APIs.
|
|
6
|
+
class ChatCompletions < Protocol
|
|
7
|
+
include ChatCompletions::Chat
|
|
8
|
+
include ChatCompletions::Embeddings
|
|
9
|
+
include ChatCompletions::Models
|
|
10
|
+
include ChatCompletions::Moderation
|
|
11
|
+
include ChatCompletions::Streaming
|
|
12
|
+
include ChatCompletions::Tools
|
|
13
|
+
include ChatCompletions::Images
|
|
14
|
+
include ChatCompletions::Media
|
|
15
|
+
include ChatCompletions::Speech
|
|
16
|
+
include ChatCompletions::Transcription
|
|
17
|
+
|
|
18
|
+
public :render_transcription_options
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
module BatchRequests # :nodoc: all
|
|
7
|
+
CHAT_FIELDS = %w[
|
|
8
|
+
messages tools temperature p frequency_penalty presence_penalty reasoning thinking_budget
|
|
9
|
+
return_prompt logprobs max_tokens max_input_tokens k seed
|
|
10
|
+
].freeze
|
|
11
|
+
EMBEDDING_FIELDS = %w[texts images input_type inputs max_tokens output_dimension embedding_types
|
|
12
|
+
truncate].freeze
|
|
13
|
+
private_constant :CHAT_FIELDS, :EMBEDDING_FIELDS
|
|
14
|
+
|
|
15
|
+
def batch_dataset_type(requests)
|
|
16
|
+
types = requests.map do |request|
|
|
17
|
+
body = request.fetch(:payload)
|
|
18
|
+
body.key?(:messages) || body.key?('messages') ? 'batch-chat-v2-input' : 'batch-embed-v2-input'
|
|
19
|
+
end.uniq
|
|
20
|
+
raise ArgumentError, 'Cohere batches cannot mix chat and embeddings' unless types.one?
|
|
21
|
+
|
|
22
|
+
types.first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_batch_request(request, type:)
|
|
26
|
+
body = JSON.parse(JSON.generate(batch_payload(request, except: :model)))
|
|
27
|
+
if type == 'batch-embed-v2-input' && body['output_dimension']
|
|
28
|
+
raise ArgumentError,
|
|
29
|
+
'Cohere batch datasets currently reject dimensions; omit dimensions to use the model default'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
render_batch_chat(body) if type == 'batch-chat-v2-input'
|
|
33
|
+
allowed = type == 'batch-chat-v2-input' ? CHAT_FIELDS : EMBEDDING_FIELDS
|
|
34
|
+
unsupported = body.keys - allowed
|
|
35
|
+
unless unsupported.empty?
|
|
36
|
+
raise ArgumentError, "Cohere batches do not support these request options: #{unsupported.join(', ')}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
custom_id = request.fetch(:custom_id)
|
|
40
|
+
custom_id = "#{custom_id}:array" if request[:text].is_a?(Array)
|
|
41
|
+
{ custom_id:, body: }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_batch_chat(body)
|
|
45
|
+
if (thinking = body.delete('thinking'))
|
|
46
|
+
body['reasoning'] = thinking['type'] != 'disabled'
|
|
47
|
+
body['thinking_budget'] = thinking['token_budget'] if thinking['token_budget']
|
|
48
|
+
end
|
|
49
|
+
Array(body['messages']).each { |message| render_batch_message(message) }
|
|
50
|
+
Array(body['tools']).each do |tool|
|
|
51
|
+
function = tool.fetch('function')
|
|
52
|
+
parameters = function['parameters']
|
|
53
|
+
function['parameters'] = JSON.generate(parameters) unless parameters.is_a?(String)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_batch_message(message)
|
|
58
|
+
content = message['content']
|
|
59
|
+
content = [{ 'type' => 'text', 'text' => content }] if content.is_a?(String)
|
|
60
|
+
message['content'] = content&.map { |part| render_batch_content(part) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_batch_content(part)
|
|
64
|
+
unless %w[text thinking image_url].include?(part['type'])
|
|
65
|
+
raise ArgumentError, "Cohere batches do not support #{part['type']} content"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
part = part.dup
|
|
69
|
+
part['image_url'] = part['image_url'].fetch('url') if part['image_url'].is_a?(Hash)
|
|
70
|
+
part
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|