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,662 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
# A Protocol knows how to talk to a family of provider APIs: rendering
|
|
7
|
+
# request payloads, parsing responses, streaming chunks, and naming the
|
|
8
|
+
# endpoints involved. Its counterpart, Provider, knows where to talk and
|
|
9
|
+
# who it is. The protocols that ship with the gem live under
|
|
10
|
+
# RubyLLM::Protocols.
|
|
11
|
+
#
|
|
12
|
+
# Subclass Protocol, or a shipped subclass such as
|
|
13
|
+
# RubyLLM::Protocols::ChatCompletions, to support a new wire format. Each
|
|
14
|
+
# operation (chat, embeddings, moderation, image generation, video
|
|
15
|
+
# generation, speech, transcription, OCR, reranking, token counting, and
|
|
16
|
+
# model listing) is served by three kinds of seam method you override:
|
|
17
|
+
#
|
|
18
|
+
# - <tt>render_*</tt> serializes a RubyLLM request into the wire payload,
|
|
19
|
+
# such as +render_payload+ for chat or +render_embedding_payload+.
|
|
20
|
+
# - <tt>*_url</tt> names the endpoint, such as +completion_url+ or
|
|
21
|
+
# +embedding_url+.
|
|
22
|
+
# - <tt>parse_*</tt> turns the wire response back into RubyLLM objects, such
|
|
23
|
+
# as +parse_completion_body+ or +parse_embedding_response+.
|
|
24
|
+
#
|
|
25
|
+
# Override the seams for the operations you support; the rest raise
|
|
26
|
+
# NotImplementedError. For example:
|
|
27
|
+
#
|
|
28
|
+
# class ChatCompletions < RubyLLM::Protocols::ChatCompletions
|
|
29
|
+
# def completion_url
|
|
30
|
+
# 'v2/chat'
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# A protocol instance is constructed by its Provider and borrows the
|
|
35
|
+
# provider's Connection, so subclasses never build HTTP clients
|
|
36
|
+
# themselves.
|
|
37
|
+
class Protocol
|
|
38
|
+
include Streaming
|
|
39
|
+
include BinaryStreaming
|
|
40
|
+
|
|
41
|
+
# The Provider this protocol talks through.
|
|
42
|
+
attr_reader :provider
|
|
43
|
+
|
|
44
|
+
# The provider's Configuration.
|
|
45
|
+
attr_reader :config
|
|
46
|
+
|
|
47
|
+
# The provider's HTTP connection. Subclasses use it to reach their
|
|
48
|
+
# endpoints.
|
|
49
|
+
attr_reader :connection
|
|
50
|
+
|
|
51
|
+
# The Model this instance targets, or +nil+ for model-less operations
|
|
52
|
+
# such as listing models.
|
|
53
|
+
attr_reader :model
|
|
54
|
+
|
|
55
|
+
# :stopdoc:
|
|
56
|
+
|
|
57
|
+
# Declares seam methods that raise NotImplementedError until a subclass
|
|
58
|
+
# overrides them: render_* serializes a request to wire form, *_url names an
|
|
59
|
+
# endpoint, and parse_* reads a wire response back into RubyLLM objects.
|
|
60
|
+
def self.abstract(*names)
|
|
61
|
+
names.each do |name|
|
|
62
|
+
define_method(name) do |*_args, **_opts|
|
|
63
|
+
raise NotImplementedError, "#{self.class} must implement ##{name}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
abstract :render_payload, :completion_url, :parse_completion_body
|
|
69
|
+
abstract :render_tool_approval_response
|
|
70
|
+
abstract :models_url, :parse_list_models_response
|
|
71
|
+
abstract :render_embedding_payload, :embedding_url, :parse_embedding_response
|
|
72
|
+
abstract :render_moderation_payload, :moderation_url, :parse_moderation_response
|
|
73
|
+
abstract :render_image_payload, :images_url, :parse_image_response
|
|
74
|
+
abstract :render_video_payload, :video_url, :parse_video_job
|
|
75
|
+
abstract :video_job_url, :parse_video_job_status, :download_video
|
|
76
|
+
abstract :render_speech_payload, :speech_url, :parse_speech_response
|
|
77
|
+
abstract :render_transcription_payload, :transcription_url, :parse_transcription_response
|
|
78
|
+
abstract :render_ocr_payload, :ocr_url, :parse_ocr_response
|
|
79
|
+
abstract :render_rerank_payload, :rerank_url, :parse_rerank_response
|
|
80
|
+
abstract :render_count_tokens_payload, :count_tokens_url, :parse_count_tokens_response
|
|
81
|
+
abstract :render_tokenization_payload, :tokenization_url, :parse_tokenization_response
|
|
82
|
+
abstract :render_compaction_payload, :compaction_url, :parse_compaction_response
|
|
83
|
+
abstract :render_cache_payload, :render_cache_update_payload, :caches_url, :cache_url, :parse_cache_response
|
|
84
|
+
|
|
85
|
+
def initialize(provider, model = nil)
|
|
86
|
+
@provider = provider
|
|
87
|
+
@config = provider.config
|
|
88
|
+
@connection = provider.connection
|
|
89
|
+
@model = model
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def tool_approval_response(tool_call, approved:)
|
|
93
|
+
Message.new(role: :tool, content: approved ? 'Approved' : 'Denied', tool_call_id: tool_call.id,
|
|
94
|
+
raw_content: render_tool_approval_response(tool_call, approved:))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def complete(messages, tools:, temperature:, provider_options: {}, headers: {}, schema: nil, thinking: nil,
|
|
98
|
+
max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
|
|
99
|
+
usage_recorder: nil, server_tools: [], compaction: nil, end_user: nil, &)
|
|
100
|
+
resolution = resolve_server_tools_for_request(server_tools)
|
|
101
|
+
headers = resolution.headers.merge(headers) if resolution
|
|
102
|
+
headers = apply_compaction_headers(headers, compaction) if compaction
|
|
103
|
+
payload = render(
|
|
104
|
+
messages, tools:, tool_prefs:, temperature:, max_output_tokens:, provider_options:, schema:, thinking:,
|
|
105
|
+
citations:, caching:, compaction:, end_user:, before_request:, server_tools:,
|
|
106
|
+
stream: block_given?
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
track_usage(:chat, on_finish: usage_recorder) do
|
|
110
|
+
if block_given?
|
|
111
|
+
stream_response(payload, headers) do |chunk|
|
|
112
|
+
@usage_tracker.observe(chunk)
|
|
113
|
+
yield chunk
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
sync_response payload, headers
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
rescue NotImplementedError
|
|
120
|
+
raise Error, "#{@provider.name} doesn't support chat"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def render(messages, tools:, temperature:, provider_options: {}, schema: nil, thinking: nil,
|
|
124
|
+
max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
|
|
125
|
+
stream: false, server_tools: [], compaction: nil, end_user: nil)
|
|
126
|
+
payload = render_payload(
|
|
127
|
+
messages,
|
|
128
|
+
tools: tools,
|
|
129
|
+
tool_prefs: tool_prefs,
|
|
130
|
+
temperature: temperature,
|
|
131
|
+
max_output_tokens: max_output_tokens,
|
|
132
|
+
model: model,
|
|
133
|
+
stream: stream,
|
|
134
|
+
schema: schema,
|
|
135
|
+
thinking: thinking,
|
|
136
|
+
citations: citations,
|
|
137
|
+
caching: caching
|
|
138
|
+
)
|
|
139
|
+
payload = apply_end_user(payload, end_user) if end_user
|
|
140
|
+
payload = apply_compaction(payload, compaction) if compaction
|
|
141
|
+
payload = Support::Utils.deep_merge(payload, provider_options)
|
|
142
|
+
payload = apply_server_tools(payload, server_tools)
|
|
143
|
+
apply_before_request_hooks(payload, before_request)
|
|
144
|
+
rescue NotImplementedError
|
|
145
|
+
raise Error, "#{@provider.name} doesn't support chat"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Writes the chat's safety identifier into the request payload and
|
|
149
|
+
# returns it. The default drops the value with a debug log, so
|
|
150
|
+
# providers with no equivalent field simply omit it. Protocols whose
|
|
151
|
+
# API accepts one override this.
|
|
152
|
+
#
|
|
153
|
+
# def apply_end_user(payload, identifier)
|
|
154
|
+
# payload.merge(safety_identifier: identifier)
|
|
155
|
+
# end
|
|
156
|
+
#
|
|
157
|
+
def apply_end_user(payload, identifier)
|
|
158
|
+
RubyLLM.logger.debug do
|
|
159
|
+
"#{@provider.name} has no safety identifier parameter, dropping #{identifier}"
|
|
160
|
+
end
|
|
161
|
+
payload
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Writes the chat's context compaction options into the request payload
|
|
165
|
+
# and returns it. +compaction+ is the provider-neutral Hash from
|
|
166
|
+
# Chat#with_compaction, whose keys are Chat::COMPACTION_OPTIONS. The
|
|
167
|
+
# default drops it with a debug log, so providers that manage context
|
|
168
|
+
# themselves simply ignore the request. Protocols whose API compacts
|
|
169
|
+
# server-side override this, mapping the options they support and
|
|
170
|
+
# logging the ones they do not.
|
|
171
|
+
#
|
|
172
|
+
# def apply_compaction(payload, compaction)
|
|
173
|
+
# payload.merge(context_management: [{ type: 'compaction' }])
|
|
174
|
+
# end
|
|
175
|
+
#
|
|
176
|
+
def apply_compaction(payload, compaction)
|
|
177
|
+
RubyLLM.logger.debug do
|
|
178
|
+
"#{@provider.name} has no context compaction parameter, dropping #{compaction.inspect}"
|
|
179
|
+
end
|
|
180
|
+
payload
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the completion request's HTTP headers with anything context
|
|
184
|
+
# compaction requires added. The default changes nothing; protocols that
|
|
185
|
+
# gate compaction behind a beta header override this.
|
|
186
|
+
def apply_compaction_headers(headers, _compaction)
|
|
187
|
+
headers
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The alias table mapping portable server tool names to this protocol's
|
|
191
|
+
# wire format. Protocols with server-tool support override this;
|
|
192
|
+
# +nil+ means the protocol has no server-tool support at all.
|
|
193
|
+
def server_tool_aliases
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def count_tokens(messages, tools:, tool_prefs: nil, thinking: nil, schema: nil, citations: false, caching: nil)
|
|
198
|
+
payload = render_count_tokens_payload(
|
|
199
|
+
messages,
|
|
200
|
+
tools: tools,
|
|
201
|
+
tool_prefs: tool_prefs,
|
|
202
|
+
model: model,
|
|
203
|
+
schema: schema,
|
|
204
|
+
thinking: thinking,
|
|
205
|
+
citations: citations,
|
|
206
|
+
caching: caching
|
|
207
|
+
)
|
|
208
|
+
parse_count_tokens_response post_count_tokens(payload)
|
|
209
|
+
rescue NotImplementedError
|
|
210
|
+
raise Error, "#{@provider.name} doesn't support token counting"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def list_models
|
|
214
|
+
response = @connection.get models_url
|
|
215
|
+
parse_list_models_response response, @provider.slug
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def tokenize(text, model:)
|
|
219
|
+
payload = render_tokenization_payload(text, model:)
|
|
220
|
+
response = @connection.post tokenization_url, payload
|
|
221
|
+
parse_tokenization_response(response, model:)
|
|
222
|
+
rescue NotImplementedError
|
|
223
|
+
raise Error, "#{@provider.name} doesn't support text tokenization"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def compact(messages, headers: {}, before_request: [], usage_recorder: nil)
|
|
227
|
+
payload = apply_before_request_hooks(render_compaction_payload(messages), before_request)
|
|
228
|
+
track_usage(:chat, on_finish: usage_recorder) do
|
|
229
|
+
response = @connection.post compaction_url, payload, usage: @usage_tracker do |request|
|
|
230
|
+
request.headers = headers.merge(request.headers) unless headers.empty?
|
|
231
|
+
end
|
|
232
|
+
parse_compaction_response(response)
|
|
233
|
+
end
|
|
234
|
+
rescue NotImplementedError
|
|
235
|
+
raise Error, "#{@provider.name} doesn't support manual compaction"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def embed(text, model:, dimensions:, task_type: nil, title: nil, with: nil, provider_options: {})
|
|
239
|
+
attachments = Attachment.wrap(with, config: @config)
|
|
240
|
+
raise UnsupportedAttachmentError, attachments.first.mime_type if attachments.any? && !supports_embedding_media?
|
|
241
|
+
|
|
242
|
+
track_usage(:embedding) do
|
|
243
|
+
payload = if attachments.any?
|
|
244
|
+
render_embedding_payload(text, model:, dimensions:, task_type:, title:, with: attachments,
|
|
245
|
+
provider_options:)
|
|
246
|
+
else
|
|
247
|
+
render_embedding_payload(text, model:, dimensions:, task_type:, title:, provider_options:)
|
|
248
|
+
end
|
|
249
|
+
response = @connection.post(embedding_url(model:), payload, usage: @usage_tracker)
|
|
250
|
+
parse_embedding_response(response, model:, text:)
|
|
251
|
+
end
|
|
252
|
+
rescue NotImplementedError
|
|
253
|
+
raise Error, "#{@provider.name} doesn't support embeddings"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def render_embedding(text, model:, dimensions: nil, task_type: nil, title: nil, provider_options: {}) # :nodoc:
|
|
257
|
+
render_embedding_payload(text, model:, dimensions:, task_type:, title:, provider_options:)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def moderate(input, model:, with: [], provider_options: {})
|
|
261
|
+
track_usage(:moderation) do
|
|
262
|
+
payload = render_moderation_payload(input, model:, with: Attachment.wrap(with, config: @config),
|
|
263
|
+
provider_options:)
|
|
264
|
+
response = @connection.post moderation_url, payload, usage: @usage_tracker
|
|
265
|
+
parse_moderation_response(response, model:)
|
|
266
|
+
end
|
|
267
|
+
rescue NotImplementedError
|
|
268
|
+
raise Error, "#{@provider.name} doesn't support moderation"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def paint(prompt, model:, size:, count: nil, with: nil, mask: nil, provider_options: {})
|
|
272
|
+
track_usage(:image) do
|
|
273
|
+
validate_paint_inputs!(with:, mask:)
|
|
274
|
+
payload = render_image_payload(prompt, model:, size:, count:, with:, mask:, provider_options:)
|
|
275
|
+
response = post_image(payload, with:, mask:)
|
|
276
|
+
images = parse_image_responses(response, model:)
|
|
277
|
+
images.each { |image| image.config = @config }
|
|
278
|
+
images.size <= 1 ? images.first : images
|
|
279
|
+
end
|
|
280
|
+
rescue NotImplementedError
|
|
281
|
+
raise Error, "#{@provider.name} doesn't support image generation"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def post_image(payload, with:, mask:)
|
|
285
|
+
@connection.post images_url(with:, mask:), payload, usage: @usage_tracker
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Returns every Image in an image generation response, as an Array. The
|
|
289
|
+
# default asks for the one image parse_image_response reads; protocols
|
|
290
|
+
# whose API can return several images per request override it. Only the
|
|
291
|
+
# first image carries the call's usage, so summing across the array
|
|
292
|
+
# gives the cost of the call.
|
|
293
|
+
def parse_image_responses(response, model:)
|
|
294
|
+
Array(parse_image_response(response, model:))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Video generation is asynchronous on every provider: this submits the
|
|
298
|
+
# job and returns a VideoJob, whose #refresh and #video poll and
|
|
299
|
+
# download through this protocol instance.
|
|
300
|
+
def animate_later(prompt, model:, with: nil, extend: nil, provider_options: {})
|
|
301
|
+
raise ArgumentError, 'with: and extend: cannot be combined' if with && extend
|
|
302
|
+
|
|
303
|
+
if extend
|
|
304
|
+
payload = render_video_extension_payload(prompt, model:, extend:, provider_options:)
|
|
305
|
+
url = video_extension_url
|
|
306
|
+
else
|
|
307
|
+
attachments = Attachment.wrap(with, config: @config)
|
|
308
|
+
validate_animate_inputs!(with: attachments)
|
|
309
|
+
payload = render_video_payload(prompt, model:, with: attachments, provider_options:)
|
|
310
|
+
url = video_request_url(payload)
|
|
311
|
+
end
|
|
312
|
+
response = post_video(url, payload)
|
|
313
|
+
parse_video_job(response, model:)
|
|
314
|
+
rescue NotImplementedError
|
|
315
|
+
raise Error, "#{@provider.name} doesn't support video generation"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def post_video(url, payload)
|
|
319
|
+
@connection.post url, payload, idempotent: false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def video_request_url(_payload)
|
|
323
|
+
video_url
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def video_extension_url
|
|
327
|
+
video_url
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def render_video_extension_payload(*)
|
|
331
|
+
raise Error, "#{@provider.name} doesn't support video extension"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def video_extension_attachment(source)
|
|
335
|
+
source = source.url || StringIO.new(source.to_blob) if source.is_a?(Video)
|
|
336
|
+
attachments = if source.respond_to?(:read)
|
|
337
|
+
[Attachment.new(source, filename: 'video.mp4', config: @config)]
|
|
338
|
+
else
|
|
339
|
+
Attachment.wrap(source, config: @config)
|
|
340
|
+
end
|
|
341
|
+
raise ArgumentError, 'extend: takes exactly one video' unless attachments.one?
|
|
342
|
+
|
|
343
|
+
attachment = attachments.first
|
|
344
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.video?
|
|
345
|
+
|
|
346
|
+
attachment
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def refresh_video_job(job)
|
|
350
|
+
parse_video_job_status @connection.get(video_job_url(job)), job: job
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def speak(input, model:, voice:, format:, provider_options: {}, &block)
|
|
354
|
+
track_usage(:speech) do
|
|
355
|
+
payload = render_speech_payload(input, model:, voice:, format:, provider_options:)
|
|
356
|
+
next stream_speech(payload, model:, voice:, format:, &block) if block
|
|
357
|
+
|
|
358
|
+
response = @connection.post speech_url(model:), payload, usage: @usage_tracker
|
|
359
|
+
parse_speech_response(response, model:, voice:, format:)
|
|
360
|
+
end
|
|
361
|
+
rescue NotImplementedError
|
|
362
|
+
raise Error, "#{@provider.name} doesn't support speech generation"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def stream_speech(*, **, &)
|
|
366
|
+
raise Error, "#{@provider.name} doesn't support streaming speech with this protocol"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def stream_speech_response(url, payload, model:, voice:, format:)
|
|
370
|
+
empty_response = Faraday::Response.new(body: '')
|
|
371
|
+
audio = parse_speech_response(empty_response, model:, voice:, format:)
|
|
372
|
+
response = stream_binary(url, payload) do |data|
|
|
373
|
+
yield SpeechChunk.new(data:, format: audio.format, mime_type: audio.mime_type)
|
|
374
|
+
end
|
|
375
|
+
parse_speech_response(response, model:, voice:, format:)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def render_transcription_options(timestamps:, **)
|
|
379
|
+
return {} if timestamps.nil?
|
|
380
|
+
|
|
381
|
+
raise ArgumentError, 'This transcription protocol does not support timestamps'
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def transcribe(audio_file, model:, language:, format: nil, speaker_names: nil,
|
|
385
|
+
speaker_references: nil, provider_options: {}, prompt: nil, temperature: nil, &block)
|
|
386
|
+
streaming = block_given?
|
|
387
|
+
track_usage(:transcription) do
|
|
388
|
+
file_part = build_audio_file_part(audio_file)
|
|
389
|
+
payload = render_transcription_payload(file_part, model:, language:, format:, speaker_names:,
|
|
390
|
+
speaker_references:, provider_options:, prompt:,
|
|
391
|
+
temperature:)
|
|
392
|
+
next stream_transcription(payload, model:, &block) if streaming
|
|
393
|
+
|
|
394
|
+
response = @connection.post transcription_url, payload, usage: @usage_tracker
|
|
395
|
+
parse_transcription_response(response, model:)
|
|
396
|
+
end
|
|
397
|
+
rescue NotImplementedError
|
|
398
|
+
raise Error, "#{@provider.name} doesn't support transcription"
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Streams a transcription, yielding TranscriptionChunk objects and
|
|
402
|
+
# returning the final Transcription. Protocols whose provider streams
|
|
403
|
+
# transcriptions override this.
|
|
404
|
+
def stream_transcription(*, **, &)
|
|
405
|
+
raise_transcription_streaming_unsupported
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def raise_transcription_streaming_unsupported # :nodoc:
|
|
409
|
+
raise Error, "#{@provider.name} doesn't support streaming transcription"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Whether the protocol can embed media attachments alongside text.
|
|
413
|
+
# Protocols that support multimodal embeddings override this and accept
|
|
414
|
+
# a +with:+ array of Attachments in render_embedding_payload.
|
|
415
|
+
def supports_embedding_media?
|
|
416
|
+
false
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def ocr(file, model:, pages: nil, provider_options: {})
|
|
420
|
+
track_usage(:ocr) do
|
|
421
|
+
payload = render_ocr_payload(file, model:, pages:, provider_options:)
|
|
422
|
+
response = @connection.post ocr_url, payload, usage: @usage_tracker
|
|
423
|
+
parse_ocr_response(response, model:)
|
|
424
|
+
end
|
|
425
|
+
rescue NotImplementedError
|
|
426
|
+
raise Error, "#{@provider.name} doesn't support OCR"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def rerank(query, documents, model:, top_n: nil, provider_options: {})
|
|
430
|
+
track_usage(:rerank) do
|
|
431
|
+
payload = render_rerank_payload(query, documents, model:, top_n:, provider_options:)
|
|
432
|
+
response = @connection.post rerank_url, payload, usage: @usage_tracker
|
|
433
|
+
parse_rerank_response(response, model:, documents:)
|
|
434
|
+
end
|
|
435
|
+
rescue NotImplementedError
|
|
436
|
+
raise Error, "#{@provider.name} doesn't support reranking"
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def cache_content(content, model:, ttl: nil, instructions: nil, with: nil)
|
|
440
|
+
payload = render_cache_payload(content, model:, ttl:, instructions:,
|
|
441
|
+
attachments: Attachment.wrap(with, config: @config))
|
|
442
|
+
response = @connection.post caches_url, payload, idempotent: false
|
|
443
|
+
parse_cache_response(response.body)
|
|
444
|
+
rescue NotImplementedError
|
|
445
|
+
raise Error, "#{@provider.name} doesn't support explicit content caching"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def find_cache(name)
|
|
449
|
+
response = @connection.get cache_url(name)
|
|
450
|
+
parse_cache_response(response.body)
|
|
451
|
+
rescue NotImplementedError
|
|
452
|
+
raise Error, "#{@provider.name} doesn't support explicit content caching"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def delete_cache(name)
|
|
456
|
+
@connection.delete cache_url(name)
|
|
457
|
+
true
|
|
458
|
+
rescue NotImplementedError
|
|
459
|
+
raise Error, "#{@provider.name} doesn't support explicit content caching"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def extend_cache(name, ttl:)
|
|
463
|
+
response = @connection.patch cache_url(name), render_cache_update_payload(ttl:)
|
|
464
|
+
parse_cache_response(response.body)
|
|
465
|
+
rescue NotImplementedError
|
|
466
|
+
raise Error, "#{@provider.name} doesn't support explicit content caching"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def parse_error(response)
|
|
470
|
+
@provider.parse_error(response)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def preprocess_message(message)
|
|
474
|
+
return message unless auto_upload_large_files?
|
|
475
|
+
return message unless message.role == :user
|
|
476
|
+
return message if message.attachments.empty?
|
|
477
|
+
|
|
478
|
+
uploaded = message.attachments.map { |attachment| preprocess_attachment(attachment) }
|
|
479
|
+
return message if uploaded == message.attachments
|
|
480
|
+
|
|
481
|
+
message.with_attachments(uploaded)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
private
|
|
485
|
+
|
|
486
|
+
def resolve_server_tools_for_request(entries)
|
|
487
|
+
return nil if entries.nil? || entries.empty?
|
|
488
|
+
|
|
489
|
+
aliases = server_tool_aliases
|
|
490
|
+
unless aliases
|
|
491
|
+
raise UnsupportedServerToolError,
|
|
492
|
+
"#{@provider.name} has no server-tool support through RubyLLM yet. " \
|
|
493
|
+
'Request options in the provider vocabulary can be set with with_provider_options.'
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
RubyLLM::Tools::ServerTools.resolve(entries, aliases: aliases, owner: @provider.name)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def apply_server_tools(payload, entries)
|
|
500
|
+
resolution = resolve_server_tools_for_request(entries)
|
|
501
|
+
return payload unless resolution
|
|
502
|
+
|
|
503
|
+
payload = Support::Utils.deep_merge(payload, resolution.payload) unless resolution.payload.empty?
|
|
504
|
+
merge_server_tool_entries(payload, resolution.tools) if resolution.tools.any?
|
|
505
|
+
payload
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Server tools join function tools in the payload's tools array. The
|
|
509
|
+
# entry shape comes from the alias table or the caller's raw Hash.
|
|
510
|
+
def merge_server_tool_entries(payload, entries)
|
|
511
|
+
payload[:tools] = Array(payload[:tools]) + entries
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def track_usage(operation, on_finish: nil)
|
|
515
|
+
@usage_tracker = Accounting::Usage::Tracker.new(
|
|
516
|
+
operation:,
|
|
517
|
+
provider: @provider,
|
|
518
|
+
model: @model,
|
|
519
|
+
config: @config,
|
|
520
|
+
on_finish:
|
|
521
|
+
)
|
|
522
|
+
result = yield
|
|
523
|
+
@usage_tracker.succeed(result)
|
|
524
|
+
result
|
|
525
|
+
rescue StandardError => e
|
|
526
|
+
@usage_tracker.fail_pending(e)
|
|
527
|
+
raise
|
|
528
|
+
ensure
|
|
529
|
+
@usage_tracker = nil
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def apply_before_request_hooks(payload, hooks)
|
|
533
|
+
Array(hooks).each { |hook| hook.call(payload) }
|
|
534
|
+
payload
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def auto_upload_large_files?
|
|
538
|
+
@config.auto_upload_large_files && @provider.files? && supports_provider_file_references?
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def supports_provider_file_references?
|
|
542
|
+
false
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def preprocess_attachment(attachment)
|
|
546
|
+
return attachment if attachment.provider_file?
|
|
547
|
+
return attachment unless upload_large_attachment?(attachment)
|
|
548
|
+
|
|
549
|
+
ensure_provider_file_size!(attachment)
|
|
550
|
+
Attachment.new(provider_upload(attachment), config: @config)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Uploads are memoized per provider on the attachment itself, so a chat
|
|
554
|
+
# that switches providers uploads once to each rather than replaying
|
|
555
|
+
# another provider's file reference. An upload past its provider
|
|
556
|
+
# retention window is replaced rather than reused.
|
|
557
|
+
def provider_upload(attachment)
|
|
558
|
+
scope = provider_upload_scope
|
|
559
|
+
upload = attachment.provider_uploads[scope]
|
|
560
|
+
return upload if upload && !upload.expired?
|
|
561
|
+
|
|
562
|
+
attachment.provider_uploads[scope] =
|
|
563
|
+
@provider.upload_file(attachment, **provider_file_upload_options(attachment))
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# A file id belongs to the account that uploaded it, so the memo is keyed
|
|
567
|
+
# by the credentials in play as well as the provider: a chat moved to
|
|
568
|
+
# another Context uploads again instead of replaying a foreign id. The
|
|
569
|
+
# credentials themselves are hashed so the attachment never carries them.
|
|
570
|
+
def provider_upload_scope
|
|
571
|
+
credentials = @provider.class.configuration_options.map { |option| @config.public_send(option) }
|
|
572
|
+
"#{@provider.slug}:#{Digest::SHA256.hexdigest(credentials.join("\0"))}"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def upload_large_attachment?(attachment)
|
|
576
|
+
size = attachment.byte_size
|
|
577
|
+
size && size > default_large_file_upload_threshold && provider_file_attachable?(attachment)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def default_large_file_upload_threshold
|
|
581
|
+
Float::INFINITY
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def provider_file_upload_limit
|
|
585
|
+
nil
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def provider_file_attachable?(_attachment)
|
|
589
|
+
false
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
def provider_file_upload_options(_attachment)
|
|
593
|
+
{}
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def ensure_provider_file_size!(attachment)
|
|
597
|
+
limit = provider_file_upload_limit
|
|
598
|
+
return unless limit && attachment.byte_size.to_i > limit
|
|
599
|
+
|
|
600
|
+
raise Error, "#{@provider.name} file uploads support files up to #{format_bytes(limit)}; " \
|
|
601
|
+
"#{attachment.filename} is #{format_bytes(attachment.byte_size)}"
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def format_bytes(bytes)
|
|
605
|
+
return 'unknown size' unless bytes
|
|
606
|
+
|
|
607
|
+
"#{(bytes.to_f / (1024 * 1024)).round(1)} MB"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def validate_paint_inputs!(with:, mask:)
|
|
611
|
+
return if with.nil? && mask.nil?
|
|
612
|
+
|
|
613
|
+
raise UnsupportedAttachmentError, 'image reference'
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def validate_animate_inputs!(with:)
|
|
617
|
+
return if with.empty?
|
|
618
|
+
|
|
619
|
+
raise UnsupportedAttachmentError, 'video reference image'
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def build_audio_file_part(audio_file)
|
|
623
|
+
require 'faraday/multipart'
|
|
624
|
+
|
|
625
|
+
attachment = audio_file.is_a?(Attachment) ? audio_file : Attachment.new(audio_file, config: @config)
|
|
626
|
+
body = attachment.path? ? File.expand_path(attachment.source) : StringIO.new(attachment.content)
|
|
627
|
+
|
|
628
|
+
Faraday::Multipart::FilePart.new(body, attachment.mime_type, audio_file_name(attachment))
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# Providers reject audio whose filename carries no extension, which a URL
|
|
632
|
+
# or an IO often has none of, so the detected format supplies one.
|
|
633
|
+
def audio_file_name(attachment)
|
|
634
|
+
name = attachment.filename.to_s
|
|
635
|
+
name = 'audio' if name.empty?
|
|
636
|
+
attachment.extension ? name : "#{name}.#{attachment.format}"
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def post_count_tokens(payload)
|
|
640
|
+
@connection.post count_tokens_url, payload
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def sync_response(payload, additional_headers = {})
|
|
644
|
+
response = @connection.post completion_url, payload, usage: @usage_tracker do |req|
|
|
645
|
+
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
|
646
|
+
end
|
|
647
|
+
parse_completion_response response
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def parse_completion_response(response)
|
|
651
|
+
body = response.body
|
|
652
|
+
if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
653
|
+
raise Error.new('Provider returned an empty response body', response:)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
message = parse_completion_body(body, raw: response)
|
|
657
|
+
raise Error.new('Provider returned no completion message', response:) unless message
|
|
658
|
+
|
|
659
|
+
message
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
end
|