ruby_llm 1.15.0 → 2.0.0.rc1
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 +87 -33
- 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 -6
- 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 +25 -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 +100 -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 -136
- data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
- data/lib/ruby_llm/active_record/message_methods.rb +119 -75
- 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 -150
- data/lib/ruby_llm/aliases.json +338 -167
- data/lib/ruby_llm/attachment.rb +217 -61
- 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 +1208 -150
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +274 -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 +141 -7
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +161 -89
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +155 -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 +43 -14
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +66 -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 +70380 -33380
- data/lib/ruby_llm/models.rb +528 -201
- 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 +540 -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 +166 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
- data/lib/ruby_llm/protocols/anthropic.rb +100 -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/protocols/chat_completions/media.rb +121 -0
- 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 +17 -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 +685 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +424 -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 +33 -20
- 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 +565 -124
- 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 +6 -15
- 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 +61 -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 +194 -42
- data/lib/ruby_llm/providers/bedrock.rb +217 -46
- 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 -8
- data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
- 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 -9
- 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 -60
- 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 +43 -0
- data/lib/ruby_llm/providers/mistral/models.rb +57 -21
- 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 +18 -6
- data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
- 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 -104
- 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 +4 -0
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +29 -21
- 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 +164 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +10 -0
- 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 -48
- 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 +17 -7
- data/lib/ruby_llm/railtie.rb +11 -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 +139 -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 +385 -4
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +340 -92
- 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 -530
- 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 -38
- data/lib/ruby_llm/connection.rb +0 -130
- data/lib/ruby_llm/content.rb +0 -77
- data/lib/ruby_llm/mime_type.rb +0 -71
- data/lib/ruby_llm/model/info.rb +0 -130
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
- 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 -57
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
- 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 -96
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
- 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 -221
- 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/media.rb +0 -84
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
- 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 -203
- data/lib/ruby_llm/streaming.rb +0 -175
- data/lib/ruby_llm/utils.rb +0 -91
- data/lib/tasks/models.rake +0 -565
- data/lib/tasks/release.rake +0 -67
- data/lib/tasks/vcr.rake +0 -124
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Embeddings methods for the Cohere v2 API integration
|
|
7
|
+
module Embeddings
|
|
8
|
+
DEFAULT_INPUT_TYPE = 'search_document'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def embedding_url(...)
|
|
13
|
+
'v2/embed'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
17
|
+
def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, with: [],
|
|
18
|
+
provider_options: {})
|
|
19
|
+
image_only = with.any? && separate_image_embeddings?(model)
|
|
20
|
+
payload = {
|
|
21
|
+
model: model,
|
|
22
|
+
input_type: task_type || (image_only ? 'image' : DEFAULT_INPUT_TYPE),
|
|
23
|
+
embedding_types: ['float'],
|
|
24
|
+
output_dimension: dimensions
|
|
25
|
+
}.compact
|
|
26
|
+
|
|
27
|
+
payload.merge!(image_only ? image_embedding_inputs(text, with) : embedding_inputs(text, with))
|
|
28
|
+
Support::Utils.deep_merge(payload, provider_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def supports_embedding_media?
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def separate_image_embeddings?(model) # :nodoc:
|
|
36
|
+
%w[embed-english-v3.0 embed-multilingual-v3.0].include?(model)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def image_embedding_inputs(text, attachments) # :nodoc:
|
|
40
|
+
raise ArgumentError, 'Cohere Embed v3 accepts text or an image, not both' unless text.nil? || text == ''
|
|
41
|
+
raise ArgumentError, 'Cohere Embed v3 accepts one image per request' unless attachments.one?
|
|
42
|
+
raise UnsupportedAttachmentError, attachments.first.mime_type unless attachments.first.image?
|
|
43
|
+
|
|
44
|
+
{ images: ["data:#{attachments.first.mime_type};base64,#{attachments.first.encoded}"] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_embedding_response(response, model:, text:)
|
|
48
|
+
data = response.body
|
|
49
|
+
vectors = data.dig('embeddings', 'float')
|
|
50
|
+
vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
|
|
51
|
+
billed = data.dig('meta', 'billed_units') || {}
|
|
52
|
+
|
|
53
|
+
Embedding.new(vectors:, model:, input_tokens: billed['input_tokens'])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# embed-v4 takes mixed text and images as `inputs`; text-only requests
|
|
57
|
+
# keep using the simpler `texts` array every Embed model accepts.
|
|
58
|
+
def embedding_inputs(text, attachments)
|
|
59
|
+
return { texts: Support::Utils.to_safe_array(text).map(&:to_s) } if attachments.empty?
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
{ inputs: [{ content: Media.format_content(text, attachments) }] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Handles formatting of media content for the Cohere v2 API
|
|
7
|
+
module Media
|
|
8
|
+
# Cohere resolves a remote image by its extension and rejects a URL
|
|
9
|
+
# without one, so those are inlined from the bytes already fetched.
|
|
10
|
+
URL_IMAGE_EXTENSIONS = %w[png jpeg jpg gif webp].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Cohere user messages accept only text and image_url blocks. Text
|
|
15
|
+
# attachments become citable documents when citations are on, so they
|
|
16
|
+
# are pulled out of the message and left out of the content blocks.
|
|
17
|
+
def format_content(content, attachments = [], citations: false)
|
|
18
|
+
parts = []
|
|
19
|
+
parts << format_text(content) if content
|
|
20
|
+
|
|
21
|
+
attachments.each do |attachment|
|
|
22
|
+
case attachment.type
|
|
23
|
+
when :image
|
|
24
|
+
parts << format_image(attachment)
|
|
25
|
+
when :text
|
|
26
|
+
parts << format_text(attachment.for_llm) unless citations
|
|
27
|
+
else
|
|
28
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
parts
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_text(text)
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: text
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def format_image(image)
|
|
43
|
+
{
|
|
44
|
+
type: 'image_url',
|
|
45
|
+
image_url: { url: image_source(image) }
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def image_source(image)
|
|
50
|
+
return image.source.to_s if image.url? && URL_IMAGE_EXTENSIONS.include?(image.extension)
|
|
51
|
+
|
|
52
|
+
"data:#{image.mime_type};base64,#{image.encoded}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Text attachments across the conversation become the request's
|
|
56
|
+
# top-level documents array, which is what Cohere cites against.
|
|
57
|
+
def format_documents(messages)
|
|
58
|
+
index = -1
|
|
59
|
+
|
|
60
|
+
messages.flat_map do |message|
|
|
61
|
+
message.attachments.select(&:text?).map do |attachment|
|
|
62
|
+
index += 1
|
|
63
|
+
{
|
|
64
|
+
id: "doc:#{index}",
|
|
65
|
+
data: { title: attachment.filename, text: attachment.content }.compact
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def documents?(messages)
|
|
72
|
+
messages.any? { |message| message.attachments.any?(&:text?) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Model information for the Cohere API
|
|
7
|
+
module Models
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
FEATURE_CAPABILITIES = {
|
|
11
|
+
'tools' => 'function_calling',
|
|
12
|
+
'tool_choice' => 'tool_choice',
|
|
13
|
+
'json_schema' => 'structured_output',
|
|
14
|
+
'json_mode' => 'json_mode',
|
|
15
|
+
'citations' => 'citations'
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# The model catalog is the one endpoint Cohere still serves from v1.
|
|
19
|
+
def models_url
|
|
20
|
+
'v1/models?page_size=1000'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse_list_models_response(response, slug)
|
|
24
|
+
Array(response.body['models']).reject { |model| model['is_deprecated'] }.map do |model_data|
|
|
25
|
+
model_id = model_data['name']
|
|
26
|
+
endpoints = Array(model_data['endpoints'])
|
|
27
|
+
|
|
28
|
+
Model.new(
|
|
29
|
+
id: model_id,
|
|
30
|
+
name: model_id,
|
|
31
|
+
provider: slug,
|
|
32
|
+
context_window: model_data['context_length']&.to_i,
|
|
33
|
+
modalities: modalities_from(endpoints),
|
|
34
|
+
capabilities: capabilities_from(endpoints, model_data['features']),
|
|
35
|
+
metadata: {
|
|
36
|
+
endpoints: endpoints,
|
|
37
|
+
default_endpoints: Array(model_data['default_endpoints']),
|
|
38
|
+
features: Array(model_data['features']),
|
|
39
|
+
finetuned: model_data['finetuned'],
|
|
40
|
+
tokenizer_url: model_data['tokenizer_url']
|
|
41
|
+
}.compact
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def modalities_from(endpoints)
|
|
47
|
+
return { input: ['image'], output: ['text'] } if endpoints.include?('parse')
|
|
48
|
+
return { input: ['audio'], output: ['text'] } if transcription_endpoint?(endpoints)
|
|
49
|
+
|
|
50
|
+
return embedding_modalities(endpoints) if embedding_endpoint?(endpoints)
|
|
51
|
+
return { input: ['text'], output: ['rerank'] } if rerank_endpoint?(endpoints)
|
|
52
|
+
|
|
53
|
+
{ input: ['text'], output: ['text'] }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def capabilities_from(endpoints, features)
|
|
57
|
+
return ['ocr'] if endpoints.include?('parse')
|
|
58
|
+
return ['transcription'] if transcription_endpoint?(endpoints)
|
|
59
|
+
return [] if embedding_endpoint?(endpoints) || rerank_endpoint?(endpoints)
|
|
60
|
+
|
|
61
|
+
features = Array(features)
|
|
62
|
+
capabilities = []
|
|
63
|
+
capabilities << 'streaming' if endpoints.intersect?(%w[chat generate])
|
|
64
|
+
reported = FEATURE_CAPABILITIES.filter_map do |feature, capability|
|
|
65
|
+
capability if features.include?(feature)
|
|
66
|
+
end
|
|
67
|
+
capabilities.concat(reported)
|
|
68
|
+
capabilities
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def embedding_endpoint?(endpoints)
|
|
72
|
+
endpoints.any? { |endpoint| endpoint.start_with?('embed') }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def embedding_modalities(endpoints)
|
|
76
|
+
input = []
|
|
77
|
+
input << 'text' if endpoints.include?('embed')
|
|
78
|
+
input << 'image' if endpoints.any? { |endpoint| endpoint.start_with?('embed_image') }
|
|
79
|
+
{ input: input.empty? ? ['text'] : input, output: ['embeddings'] }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rerank_endpoint?(endpoints)
|
|
83
|
+
endpoints.any? { |endpoint| endpoint.start_with?('rerank') }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def transcription_endpoint?(endpoints)
|
|
87
|
+
endpoints.any? { |endpoint| endpoint.start_with?('transcri') }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Cohere Parse accepts one document image and returns Markdown or blocks.
|
|
7
|
+
module OCR
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def ocr_url
|
|
11
|
+
'v2/parse'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_ocr_payload(file, model:, pages: nil, provider_options: {})
|
|
15
|
+
attachment = file.is_a?(Attachment) ? file : Attachment.new(file, config: @config)
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
17
|
+
raise ArgumentError, 'Cohere Parse accepts one image; pages must be [0]' unless pages.nil? || pages == [0]
|
|
18
|
+
|
|
19
|
+
reference = attachment.url? ? attachment.source.to_s : attachment.for_llm
|
|
20
|
+
{
|
|
21
|
+
model: model,
|
|
22
|
+
document: { type: 'image_url', image_url: reference },
|
|
23
|
+
output_format: 'markdown'
|
|
24
|
+
}.merge(provider_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_ocr_response(response, model:)
|
|
28
|
+
data = response.body
|
|
29
|
+
RubyLLM::OCR.new(
|
|
30
|
+
pages: data.fetch('pages').map { |page| parse_ocr_page(page) },
|
|
31
|
+
model: model,
|
|
32
|
+
usage: data.dig('meta', 'billed_units'),
|
|
33
|
+
raw: data
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_ocr_page(page)
|
|
38
|
+
content = page['markdown'] || parse_ocr_blocks(page.fetch('blocks'))
|
|
39
|
+
RubyLLM::OCR::Page.new(
|
|
40
|
+
index: page['index'], markdown: content['content'],
|
|
41
|
+
images: content['images'], tables: content['tables'], raw: page
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_ocr_blocks(blocks)
|
|
46
|
+
{
|
|
47
|
+
'content' => blocks.filter_map { |block| parse_ocr_block_text(block) }.join("\n\n"),
|
|
48
|
+
'images' => blocks.filter_map { |block| block['image'] },
|
|
49
|
+
'tables' => blocks.filter_map { |block| block['table'] }
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def parse_ocr_block_text(block)
|
|
54
|
+
case block['type']
|
|
55
|
+
when 'text' then block.dig('text', 'content')
|
|
56
|
+
when 'table' then block.dig('table', 'html')
|
|
57
|
+
when 'image' then "})"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Rerank methods for the Cohere v2 API integration
|
|
7
|
+
module Rerank
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def rerank_url
|
|
11
|
+
'v2/rerank'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
|
|
15
|
+
{
|
|
16
|
+
model: model,
|
|
17
|
+
query: query,
|
|
18
|
+
documents: documents,
|
|
19
|
+
top_n: top_n
|
|
20
|
+
}.compact.merge(provider_options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Cohere returns positions and scores only, so the ranked text comes
|
|
24
|
+
# back from the documents that were sent.
|
|
25
|
+
def parse_rerank_response(response, model:, documents: [])
|
|
26
|
+
data = response.body
|
|
27
|
+
billed = data.dig('meta', 'billed_units') || {}
|
|
28
|
+
tokens = data.dig('meta', 'tokens') || {}
|
|
29
|
+
|
|
30
|
+
RubyLLM::Rerank.new(
|
|
31
|
+
results: parse_rerank_results(data, documents),
|
|
32
|
+
model: model,
|
|
33
|
+
raw: data,
|
|
34
|
+
input_tokens: tokens['input_tokens'] || billed['input_tokens']
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse_rerank_results(data, documents)
|
|
39
|
+
Array(data['results']).map do |result|
|
|
40
|
+
index = result['index']
|
|
41
|
+
|
|
42
|
+
RubyLLM::Rerank::Result.new(
|
|
43
|
+
index: index,
|
|
44
|
+
document: result.dig('document', 'text') || (index && documents[index]),
|
|
45
|
+
score: result['relevance_score']
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Streaming methods of the Cohere v2 API integration
|
|
7
|
+
module Streaming
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def stream_url
|
|
11
|
+
completion_url
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def build_chunk(data)
|
|
15
|
+
type = data['type']
|
|
16
|
+
track_content_lengths(data, type)
|
|
17
|
+
|
|
18
|
+
Chunk.new(
|
|
19
|
+
role: :assistant,
|
|
20
|
+
model: model&.id,
|
|
21
|
+
content: extract_content_delta(data, type),
|
|
22
|
+
citations: extract_citations_delta(data, type),
|
|
23
|
+
thinking: Thinking.build(text: extract_thinking_delta(data, type)),
|
|
24
|
+
tool_calls: extract_tool_calls(data, type),
|
|
25
|
+
finish_reason: normalize_finish_reason(data.dig('delta', 'finish_reason')),
|
|
26
|
+
**stream_usage(data.dig('delta', 'usage'))
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Citation spans are offsets into their own content block, so the
|
|
31
|
+
# running length of every earlier text block is what turns them into
|
|
32
|
+
# offsets into the accumulated response content.
|
|
33
|
+
def track_content_lengths(data, type)
|
|
34
|
+
return unless type == 'content-delta'
|
|
35
|
+
|
|
36
|
+
text = data.dig('delta', 'message', 'content', 'text')
|
|
37
|
+
return unless text
|
|
38
|
+
|
|
39
|
+
@stream_content_lengths ||= Hash.new(0)
|
|
40
|
+
@stream_content_lengths[data['index']] += text.length
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def content_offset(index)
|
|
44
|
+
return 0 unless @stream_content_lengths
|
|
45
|
+
|
|
46
|
+
@stream_content_lengths.sum { |block_index, length| block_index < index ? length : 0 }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def extract_content_delta(data, type)
|
|
50
|
+
data.dig('delta', 'message', 'content', 'text') if type == 'content-delta'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_thinking_delta(data, type)
|
|
54
|
+
case type
|
|
55
|
+
when 'content-delta' then data.dig('delta', 'message', 'content', 'thinking')
|
|
56
|
+
when 'tool-plan-delta' then data.dig('delta', 'message', 'tool_plan')
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_citations_delta(data, type)
|
|
61
|
+
return nil unless type == 'citation-start'
|
|
62
|
+
|
|
63
|
+
citation = data.dig('delta', 'message', 'citations')
|
|
64
|
+
return nil unless citation
|
|
65
|
+
|
|
66
|
+
index = citation['content_index'] || 0
|
|
67
|
+
[Chat.parse_citation(citation, { index => content_offset(index) })]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extract_tool_calls(data, type)
|
|
71
|
+
case type
|
|
72
|
+
when 'tool-call-start' then stream_tool_call_start(data)
|
|
73
|
+
when 'tool-call-delta' then stream_tool_call_delta(data)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def stream_tool_call_start(data)
|
|
78
|
+
tool_call = data.dig('delta', 'message', 'tool_calls')
|
|
79
|
+
return nil unless tool_call
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
data['index'] => ToolCall.new(
|
|
83
|
+
id: tool_call['id'],
|
|
84
|
+
name: tool_call.dig('function', 'name'),
|
|
85
|
+
arguments: tool_call.dig('function', 'arguments').to_s
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stream_tool_call_delta(data)
|
|
91
|
+
arguments = data.dig('delta', 'message', 'tool_calls', 'function', 'arguments')
|
|
92
|
+
return nil unless arguments
|
|
93
|
+
|
|
94
|
+
{ data['index'] => ToolCall.new(id: nil, name: nil, arguments: arguments) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def stream_usage(usage)
|
|
98
|
+
return {} unless usage
|
|
99
|
+
|
|
100
|
+
Chat.usage_tokens(usage)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Cohere's plain-text tokenizer is served by the v1 API.
|
|
7
|
+
module Tokenization
|
|
8
|
+
def tokenization_url
|
|
9
|
+
'v1/tokenize'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render_tokenization_payload(text, model:)
|
|
13
|
+
{ model:, text: }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_tokenization_response(response, model:)
|
|
17
|
+
RubyLLM::Tokenization.new(ids: response.body.fetch('tokens'), model:, raw: response.body)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Protocols
|
|
7
|
+
class Cohere
|
|
8
|
+
# Tools methods of the Cohere v2 API integration
|
|
9
|
+
module Tools
|
|
10
|
+
EMPTY_PARAMETERS_SCHEMA = {
|
|
11
|
+
'type' => 'object',
|
|
12
|
+
'properties' => {},
|
|
13
|
+
'required' => []
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def function_for(tool)
|
|
19
|
+
definition = {
|
|
20
|
+
type: 'function',
|
|
21
|
+
function: {
|
|
22
|
+
name: tool.name,
|
|
23
|
+
description: tool.description,
|
|
24
|
+
parameters: parameters_schema_for(tool)
|
|
25
|
+
}.compact
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return definition if tool.provider_options.empty?
|
|
29
|
+
|
|
30
|
+
RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Cohere takes plain JSON Schema here and controls strictness with the
|
|
34
|
+
# request's own strict_tools flag, so the OpenAI strict keyword goes.
|
|
35
|
+
def parameters_schema_for(tool)
|
|
36
|
+
schema = tool.parameters_schema ||
|
|
37
|
+
RubyLLM::Tool::SchemaDefinition.from_parameters(tool.declared_parameters)&.json_schema
|
|
38
|
+
return EMPTY_PARAMETERS_SCHEMA unless schema
|
|
39
|
+
|
|
40
|
+
schema = RubyLLM::Support::Utils.deep_dup(schema)
|
|
41
|
+
schema.delete(:strict)
|
|
42
|
+
schema.delete('strict')
|
|
43
|
+
schema
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Cohere only forces tool use on or off; it cannot pin a named tool.
|
|
47
|
+
def build_tool_choice(choice)
|
|
48
|
+
case choice
|
|
49
|
+
when nil, :auto then nil
|
|
50
|
+
when :none then 'NONE'
|
|
51
|
+
when :required then 'REQUIRED'
|
|
52
|
+
else
|
|
53
|
+
raise ArgumentError,
|
|
54
|
+
"Cohere tool choice accepts :auto, :required, or :none, got #{choice.inspect}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_tool_calls(tool_calls)
|
|
59
|
+
tool_calls.map do |_, tool_call|
|
|
60
|
+
{
|
|
61
|
+
id: tool_call.id,
|
|
62
|
+
type: 'function',
|
|
63
|
+
function: {
|
|
64
|
+
name: tool_call.name,
|
|
65
|
+
arguments: JSON.generate(tool_call.arguments)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_tool_result(msg)
|
|
72
|
+
{
|
|
73
|
+
role: 'tool',
|
|
74
|
+
tool_call_id: msg.tool_call_id,
|
|
75
|
+
content: format_tool_result_content(msg)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Search results become document content blocks, the shape Cohere
|
|
80
|
+
# cites tool output against. A tool that attaches files sends its
|
|
81
|
+
# text and each attachment as blocks, so the model reads what the
|
|
82
|
+
# tool fetched rather than only the sentence describing it.
|
|
83
|
+
def format_tool_result_content(msg)
|
|
84
|
+
search_results = RubyLLM::SearchResults.from_content(msg.content)
|
|
85
|
+
return document_blocks(search_results) if search_results
|
|
86
|
+
return msg.content.to_s if msg.attachments.empty?
|
|
87
|
+
|
|
88
|
+
[Media.format_text(msg.content.to_s)] + msg.attachments.map do |attachment|
|
|
89
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.type == :text
|
|
90
|
+
|
|
91
|
+
Media.format_text(attachment.for_llm)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def document_blocks(search_results)
|
|
96
|
+
search_results.results.map.with_index do |result, index|
|
|
97
|
+
{
|
|
98
|
+
type: 'document',
|
|
99
|
+
document: {
|
|
100
|
+
id: result[:url] || "tool:#{index}",
|
|
101
|
+
data: { title: result[:title], text: result[:text], url: result[:url] }.compact
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_tool_calls(tool_calls, response: nil, finish_reason: nil)
|
|
108
|
+
return nil unless tool_calls&.any?
|
|
109
|
+
|
|
110
|
+
tool_calls.to_h do |tool_call|
|
|
111
|
+
[
|
|
112
|
+
tool_call['id'],
|
|
113
|
+
ToolCall.new(
|
|
114
|
+
id: tool_call['id'],
|
|
115
|
+
name: tool_call.dig('function', 'name'),
|
|
116
|
+
arguments: parse_arguments(tool_call.dig('function', 'arguments'), response, finish_reason)
|
|
117
|
+
)
|
|
118
|
+
]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_arguments(arguments, response, finish_reason)
|
|
123
|
+
return {} if arguments.nil? || arguments.empty?
|
|
124
|
+
|
|
125
|
+
JSON.parse(arguments)
|
|
126
|
+
rescue JSON::ParserError => e
|
|
127
|
+
raise ToolCallParseError.new(response: response, finish_reason: finish_reason), cause: e
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Audio transcription methods for the Cohere v2 API integration
|
|
7
|
+
module Transcription
|
|
8
|
+
DEFAULT_LANGUAGE = 'en'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def transcription_url
|
|
13
|
+
'v2/audio/transcriptions'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Cohere parses the multipart body as it streams, so every form field
|
|
17
|
+
# has to be written before the file part or the request is rejected as
|
|
18
|
+
# missing them. The file goes in last, after provider options.
|
|
19
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
20
|
+
def render_transcription_payload(file_part, model:, language:, format: nil, speaker_names: nil,
|
|
21
|
+
speaker_references: nil, provider_options: {}, prompt: nil,
|
|
22
|
+
temperature: nil)
|
|
23
|
+
{
|
|
24
|
+
model: model,
|
|
25
|
+
language: language || DEFAULT_LANGUAGE,
|
|
26
|
+
temperature: temperature
|
|
27
|
+
}.compact.merge(provider_options).merge(file: file_part)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Cohere Transcribe returns the transcript alone: no timestamps,
|
|
31
|
+
# segments, or speaker labels.
|
|
32
|
+
def parse_transcription_response(response, model:)
|
|
33
|
+
data = response.body
|
|
34
|
+
return RubyLLM::Transcription.new(text: data, model: model) if data.is_a?(String)
|
|
35
|
+
|
|
36
|
+
RubyLLM::Transcription.new(text: data['text'], model: model)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The Cohere v2 API, a native wire format shared by Cohere's own host and
|
|
6
|
+
# the platforms that serve Command, Embed, Rerank, and Transcribe models.
|
|
7
|
+
class Cohere < Protocol
|
|
8
|
+
include Cohere::Batches
|
|
9
|
+
include Cohere::Chat
|
|
10
|
+
include Cohere::Embeddings
|
|
11
|
+
include Cohere::Media
|
|
12
|
+
include Cohere::Models
|
|
13
|
+
include Cohere::OCR
|
|
14
|
+
include Cohere::Tokenization
|
|
15
|
+
include Cohere::Rerank
|
|
16
|
+
include Cohere::Streaming
|
|
17
|
+
include Cohere::Tools
|
|
18
|
+
include Cohere::Transcription
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|