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
data/lib/ruby_llm/error.rb
CHANGED
|
@@ -1,48 +1,184 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Error is the base class for provider-operation errors raised by
|
|
5
|
+
# RubyLLM, including API, network, capability, and provider response-shape
|
|
6
|
+
# failures. When an HTTP response is available, it wraps that response and
|
|
7
|
+
# normalizes the message across providers. Subclasses map common HTTP
|
|
8
|
+
# status codes: BadRequestError (400), UnauthorizedError (401),
|
|
9
|
+
# PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
|
|
10
|
+
# ServerError (500), ServiceUnavailableError (502 to 504), and
|
|
11
|
+
# OverloadedError (529).
|
|
12
|
+
#
|
|
13
|
+
# begin
|
|
14
|
+
# RubyLLM.chat.ask "Translate 'hello' to French."
|
|
15
|
+
# rescue RubyLLM::RateLimitError
|
|
16
|
+
# puts "Rate limit hit. Please wait a moment."
|
|
17
|
+
# rescue RubyLLM::Error => e
|
|
18
|
+
# puts "API error: #{e.message}"
|
|
19
|
+
# puts e.response&.status
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Local setup and programming errors, such as ConfigurationError and
|
|
23
|
+
# ModelNotFoundError, inherit from StandardError directly and are not
|
|
24
|
+
# caught by rescuing Error.
|
|
6
25
|
class Error < StandardError
|
|
26
|
+
# The HTTP response that caused the error, or +nil+ when none is
|
|
27
|
+
# available. Its +status+ and +body+ carry the provider's reply.
|
|
7
28
|
attr_reader :response
|
|
8
29
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
response = nil
|
|
13
|
-
end
|
|
30
|
+
def self.default_message # :nodoc:
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
14
33
|
|
|
34
|
+
# Creates an error with +message+. Pass +response:+ to attach the HTTP
|
|
35
|
+
# response; its body supplies the message when +message+ is +nil+.
|
|
36
|
+
def initialize(message = nil, response: nil)
|
|
15
37
|
@response = response
|
|
16
|
-
super(message || response&.body)
|
|
38
|
+
super(message || response&.body || self.class.default_message)
|
|
17
39
|
end
|
|
18
40
|
end
|
|
19
41
|
|
|
20
|
-
#
|
|
42
|
+
# Raised when a deprecated API is used and
|
|
43
|
+
# Configuration#deprecation_behavior is +:raise+. With the default
|
|
44
|
+
# +:warn+, deprecations are logged instead.
|
|
45
|
+
#
|
|
46
|
+
# RubyLLM.configure do |config|
|
|
47
|
+
# config.deprecation_behavior = :raise
|
|
48
|
+
# end
|
|
49
|
+
class DeprecationError < StandardError; end
|
|
50
|
+
|
|
51
|
+
# Raised when required configuration, such as a provider API key, is
|
|
52
|
+
# missing.
|
|
21
53
|
class ConfigurationError < StandardError; end
|
|
54
|
+
|
|
55
|
+
# Raised when RubyLLM.render_prompt cannot find the named prompt file.
|
|
22
56
|
class PromptNotFoundError < StandardError; end
|
|
57
|
+
|
|
58
|
+
# Raised when a message role outside +:system+, +:user+, +:assistant+, or
|
|
59
|
+
# +:tool+ is used.
|
|
23
60
|
class InvalidRoleError < StandardError; end
|
|
61
|
+
|
|
62
|
+
# Raised when the +choice:+ option of Chat#with_tool_options is neither a
|
|
63
|
+
# known mode nor the name of a registered tool.
|
|
24
64
|
class InvalidToolChoiceError < StandardError; end
|
|
65
|
+
|
|
66
|
+
# Raised when a user message is staged while the last response still has
|
|
67
|
+
# unanswered tool calls. Providers reject such a transcript, so the chat
|
|
68
|
+
# refuses it up front: finish the round with #complete, recording
|
|
69
|
+
# #approve or #deny decisions for calls that require approval.
|
|
70
|
+
class PendingToolCallsError < StandardError; end
|
|
71
|
+
|
|
72
|
+
# Raised when a requested model id is not in the model registry.
|
|
25
73
|
class ModelNotFoundError < StandardError; end
|
|
26
74
|
|
|
27
|
-
# Raised when
|
|
28
|
-
class
|
|
29
|
-
|
|
75
|
+
# Raised when a model registry cannot be fetched, parsed, or persisted.
|
|
76
|
+
class ModelRegistryError < StandardError; end
|
|
77
|
+
|
|
78
|
+
# Raised when an in-flight chat operation is cancelled with Chat#cancel.
|
|
79
|
+
class CancelledError < StandardError
|
|
80
|
+
# Creates an error for a cancelled chat operation.
|
|
81
|
+
def initialize(message = 'Chat generation cancelled')
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Raised when Chat#with_server_tools is used with a provider RubyLLM has
|
|
87
|
+
# no server-tool support for, or with an alias the provider's protocol
|
|
88
|
+
# does not define.
|
|
89
|
+
class UnsupportedServerToolError < Error; end
|
|
30
90
|
|
|
31
|
-
|
|
91
|
+
# Raised when an attachment cannot be formatted for the selected provider,
|
|
92
|
+
# for example an audio file sent to a model without audio input.
|
|
93
|
+
class UnsupportedAttachmentError < Error
|
|
94
|
+
GUIDANCE = 'Consider using a model that supports this attachment type.' # :nodoc:
|
|
95
|
+
|
|
96
|
+
def initialize(type = nil) # :nodoc:
|
|
32
97
|
message = 'Unsupported attachment type'
|
|
33
98
|
message = "#{message}: #{type}" if type
|
|
34
99
|
super("#{message}. #{GUIDANCE}")
|
|
35
100
|
end
|
|
36
101
|
end
|
|
37
102
|
|
|
38
|
-
#
|
|
39
|
-
class BadRequestError < Error
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class
|
|
103
|
+
# Raised for HTTP 400 responses when the request is invalid.
|
|
104
|
+
class BadRequestError < Error
|
|
105
|
+
def self.default_message # :nodoc:
|
|
106
|
+
'Invalid request - please check your input'
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Raised for HTTP 403 responses when the API key lacks permission for the
|
|
111
|
+
# requested resource.
|
|
112
|
+
class ForbiddenError < Error
|
|
113
|
+
def self.default_message # :nodoc:
|
|
114
|
+
'Forbidden - you do not have permission to access this resource'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Raised when the request exceeds the model's context window or token
|
|
119
|
+
# limits.
|
|
120
|
+
class ContextLengthExceededError < Error
|
|
121
|
+
def self.default_message # :nodoc:
|
|
122
|
+
'Context length exceeded'
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Raised when a provider returns tool-call arguments that are not valid
|
|
127
|
+
# JSON, often because the response was truncated mid-tool-call.
|
|
128
|
+
class ToolCallParseError < Error
|
|
129
|
+
# Returns the normalized reason generation stopped, or +nil+.
|
|
130
|
+
attr_reader :finish_reason
|
|
131
|
+
|
|
132
|
+
def initialize(message = nil, response: nil, finish_reason: nil) # :nodoc:
|
|
133
|
+
@finish_reason = finish_reason
|
|
134
|
+
message ||= 'Provider returned malformed tool call arguments'
|
|
135
|
+
message = "#{message} (finish_reason: #{finish_reason})" if finish_reason
|
|
136
|
+
super(message, response: response)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Raised for HTTP 529 responses when the provider is temporarily
|
|
141
|
+
# overloaded.
|
|
142
|
+
class OverloadedError < Error
|
|
143
|
+
def self.default_message # :nodoc:
|
|
144
|
+
'Service overloaded - please try again later'
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Raised for HTTP 402 responses when the provider account has a billing
|
|
149
|
+
# or quota problem.
|
|
150
|
+
class PaymentRequiredError < Error
|
|
151
|
+
def self.default_message # :nodoc:
|
|
152
|
+
'Payment required - please top up your account'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Raised for HTTP 429 responses when the provider rate limit is exceeded.
|
|
157
|
+
class RateLimitError < Error
|
|
158
|
+
def self.default_message # :nodoc:
|
|
159
|
+
'Rate limit exceeded - please wait a moment'
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Raised for HTTP 500 responses when the provider reports a server error.
|
|
164
|
+
class ServerError < Error
|
|
165
|
+
def self.default_message # :nodoc:
|
|
166
|
+
'API server error - please try again'
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Raised for HTTP 502, 503, and 504 responses when the provider is
|
|
171
|
+
# temporarily unavailable.
|
|
172
|
+
class ServiceUnavailableError < Error
|
|
173
|
+
def self.default_message # :nodoc:
|
|
174
|
+
'API server unavailable - please try again later'
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Raised for HTTP 401 responses when the API key is missing or invalid.
|
|
179
|
+
class UnauthorizedError < Error
|
|
180
|
+
def self.default_message # :nodoc:
|
|
181
|
+
'Invalid API key - check your credentials'
|
|
182
|
+
end
|
|
183
|
+
end
|
|
48
184
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A Fallback is a fallback model target configured with Chat#with_fallbacks.
|
|
5
|
+
# When the active model fails with a matching error, the chat retries the
|
|
6
|
+
# request with each fallback in order. Instances are yielded to
|
|
7
|
+
# Chat#before_fallback and Chat#after_fallback callbacks, enriched with the
|
|
8
|
+
# details of that attempt.
|
|
9
|
+
#
|
|
10
|
+
# chat = RubyLLM.chat(model: "gpt-4.1")
|
|
11
|
+
# .with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5")
|
|
12
|
+
#
|
|
13
|
+
# chat.before_fallback do |fallback|
|
|
14
|
+
# puts "Falling back from #{fallback.from.id} to #{fallback.to.id}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# chat.after_fallback do |fallback|
|
|
18
|
+
# puts "Fallback #{fallback.succeeded? ? 'succeeded' : 'failed'}"
|
|
19
|
+
# end
|
|
20
|
+
class Fallback
|
|
21
|
+
include Support::Inspectable
|
|
22
|
+
|
|
23
|
+
# The error classes that trigger a fallback when Chat#with_fallbacks is
|
|
24
|
+
# called without +on:+.
|
|
25
|
+
DEFAULT_ERRORS = [
|
|
26
|
+
RateLimitError,
|
|
27
|
+
ServerError,
|
|
28
|
+
ServiceUnavailableError,
|
|
29
|
+
OverloadedError,
|
|
30
|
+
Faraday::TimeoutError,
|
|
31
|
+
Faraday::ConnectionFailed
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# The model id of the fallback target.
|
|
35
|
+
attr_reader :id
|
|
36
|
+
|
|
37
|
+
# The provider slug of the fallback target as a String, or +nil+.
|
|
38
|
+
attr_reader :provider
|
|
39
|
+
|
|
40
|
+
# The Model object the fallback was configured with, or +nil+ when it was
|
|
41
|
+
# configured with a model id.
|
|
42
|
+
attr_reader :model
|
|
43
|
+
|
|
44
|
+
# The Chat performing the fallback attempt.
|
|
45
|
+
attr_reader :chat
|
|
46
|
+
|
|
47
|
+
# The error that triggered the fallback.
|
|
48
|
+
attr_reader :error
|
|
49
|
+
|
|
50
|
+
# The Model the chat is falling back from.
|
|
51
|
+
attr_reader :from
|
|
52
|
+
|
|
53
|
+
# The Model the chat is falling back to.
|
|
54
|
+
attr_reader :to
|
|
55
|
+
|
|
56
|
+
# The fallback attempt number, starting at 1.
|
|
57
|
+
attr_reader :attempt
|
|
58
|
+
|
|
59
|
+
# The response Message from a successful fallback attempt, or +nil+.
|
|
60
|
+
attr_reader :response
|
|
61
|
+
|
|
62
|
+
# The error raised by the fallback attempt itself, or +nil+ if it
|
|
63
|
+
# succeeded.
|
|
64
|
+
attr_reader :fallback_error
|
|
65
|
+
|
|
66
|
+
attr_reader :streaming # :nodoc:
|
|
67
|
+
|
|
68
|
+
def self.build(value) # :nodoc:
|
|
69
|
+
case value
|
|
70
|
+
when self
|
|
71
|
+
value
|
|
72
|
+
when RubyLLM::Model
|
|
73
|
+
new(model: value)
|
|
74
|
+
when String, Symbol
|
|
75
|
+
new(id: value.to_s)
|
|
76
|
+
else
|
|
77
|
+
raise ArgumentError, 'Expected a model id or RubyLLM::Model'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inspect_attributes # :nodoc:
|
|
82
|
+
{ id: id, provider: provider, attempt: attempt }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def initialize(id: nil, provider: nil, model: nil, **attributes) # :nodoc:
|
|
86
|
+
@id = id || model&.id
|
|
87
|
+
@provider = (provider || model&.provider)&.to_s
|
|
88
|
+
@model = model
|
|
89
|
+
@chat = attributes[:chat]
|
|
90
|
+
@error = attributes[:error]
|
|
91
|
+
@from = attributes[:from]
|
|
92
|
+
@to = attributes[:to]
|
|
93
|
+
@attempt = attributes[:attempt]
|
|
94
|
+
@streaming = attributes.fetch(:streaming, false)
|
|
95
|
+
@chunks_yielded = attributes.fetch(:chunks_yielded, false)
|
|
96
|
+
@response = attributes[:response]
|
|
97
|
+
@fallback_error = attributes[:fallback_error]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def with_attempt(**attributes) # :nodoc:
|
|
101
|
+
self.class.new(id: id, provider: provider, model: model, **attributes)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def finish(response: nil, fallback_error: nil) # :nodoc:
|
|
105
|
+
@response = response
|
|
106
|
+
@fallback_error = fallback_error
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns whether the fallback happened during a streaming request.
|
|
111
|
+
def streaming?
|
|
112
|
+
streaming
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns whether the failed model already yielded stream chunks before
|
|
116
|
+
# the fallback.
|
|
117
|
+
def chunks_yielded?
|
|
118
|
+
@chunks_yielded
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns +true+ if the fallback attempt raised an error, +false+
|
|
122
|
+
# otherwise.
|
|
123
|
+
def failed?
|
|
124
|
+
!fallback_error.nil?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns +true+ if the fallback attempt produced a response without
|
|
128
|
+
# error, +false+ otherwise.
|
|
129
|
+
def succeeded?
|
|
130
|
+
!response.nil? && !failed?
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'marcel'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Files # :nodoc:
|
|
7
|
+
module MimeType # :nodoc: all
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def for(...)
|
|
11
|
+
Marcel::MimeType.for(...)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def image?(type)
|
|
15
|
+
type.start_with?('image/')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def video?(type)
|
|
19
|
+
type.start_with?('video/')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def audio?(type)
|
|
23
|
+
type.start_with?('audio/')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pdf?(type)
|
|
27
|
+
type == 'application/pdf'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def document?(type)
|
|
31
|
+
return false if pdf?(type) || text?(type)
|
|
32
|
+
|
|
33
|
+
DOCUMENT_MIME_TYPES.include?(type) ||
|
|
34
|
+
DOCUMENT_MIME_PREFIXES.any? { |prefix| type.start_with?(prefix) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def text?(type)
|
|
38
|
+
type.start_with?('text/') ||
|
|
39
|
+
TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
|
|
40
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Structured-syntax suffixes (+json, +xml, ...) that mark a non-text/ type as text.
|
|
44
|
+
TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
|
|
45
|
+
|
|
46
|
+
# MIME types that don't have a text/ prefix but should be treated as text
|
|
47
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
|
|
48
|
+
'application/json', # Base type, even if specific ones end with +json
|
|
49
|
+
'application/xml', # Base type, even if specific ones end with +xml
|
|
50
|
+
'application/javascript',
|
|
51
|
+
'application/ecmascript',
|
|
52
|
+
'application/rtf',
|
|
53
|
+
'application/sql',
|
|
54
|
+
'application/x-sh',
|
|
55
|
+
'application/x-csh',
|
|
56
|
+
'application/x-httpd-php',
|
|
57
|
+
'application/sdp',
|
|
58
|
+
'application/sparql-query',
|
|
59
|
+
'application/graphql',
|
|
60
|
+
'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
|
|
61
|
+
'application/mbox', # Mailbox format
|
|
62
|
+
'application/x-tex',
|
|
63
|
+
'application/x-latex',
|
|
64
|
+
'application/x-perl',
|
|
65
|
+
'application/x-python',
|
|
66
|
+
'application/x-tcl',
|
|
67
|
+
'application/pgp-signature', # Often ASCII armored
|
|
68
|
+
'application/pgp-keys', # Often ASCII armored
|
|
69
|
+
'application/vnd.coffeescript',
|
|
70
|
+
'application/vnd.dart',
|
|
71
|
+
'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
|
|
72
|
+
'application/vnd.zul', # ZK User Interface Language (can be XML-like)
|
|
73
|
+
'application/x-yaml', # Common non-standard for YAML
|
|
74
|
+
'application/yaml', # Standard for YAML
|
|
75
|
+
'application/toml' # TOML configuration files
|
|
76
|
+
].freeze
|
|
77
|
+
|
|
78
|
+
DOCUMENT_MIME_TYPES = [
|
|
79
|
+
'application/msword',
|
|
80
|
+
'application/rtf',
|
|
81
|
+
'application/vnd.apple.keynote',
|
|
82
|
+
'application/vnd.apple.numbers',
|
|
83
|
+
'application/vnd.apple.pages',
|
|
84
|
+
'application/vnd.google-apps.document',
|
|
85
|
+
'application/vnd.google-apps.presentation',
|
|
86
|
+
'application/vnd.google-apps.spreadsheet',
|
|
87
|
+
'application/vnd.ms-excel',
|
|
88
|
+
'application/vnd.ms-powerpoint'
|
|
89
|
+
].freeze
|
|
90
|
+
|
|
91
|
+
DOCUMENT_MIME_PREFIXES = [
|
|
92
|
+
'application/vnd.openxmlformats-officedocument.',
|
|
93
|
+
'application/vnd.oasis.opendocument.'
|
|
94
|
+
].freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -3,82 +3,203 @@
|
|
|
3
3
|
require 'base64'
|
|
4
4
|
|
|
5
5
|
module RubyLLM
|
|
6
|
-
#
|
|
6
|
+
# An Image is a generated or edited image. Save it to a file with #save
|
|
7
|
+
# or read its bytes with #to_blob. Both handle hosted URLs and inline data.
|
|
8
|
+
#
|
|
9
|
+
# image = RubyLLM.paint("a sunset over mountains in watercolor style")
|
|
10
|
+
# image.save("sunset.png")
|
|
11
|
+
#
|
|
7
12
|
class Image
|
|
8
|
-
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
include Accounting::Usage::Result
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
# The URL of the hosted image, for providers that return one, or +nil+.
|
|
17
|
+
attr_reader :url
|
|
18
|
+
|
|
19
|
+
# The Base64-encoded image data, for providers that return the image
|
|
20
|
+
# inline, or +nil+.
|
|
21
|
+
attr_reader :data
|
|
22
|
+
|
|
23
|
+
# The MIME type of the image data, such as <tt>"image/png"</tt>.
|
|
24
|
+
attr_reader :mime_type
|
|
25
|
+
|
|
26
|
+
# The provider's rewritten version of the prompt, when reported.
|
|
27
|
+
attr_reader :revised_prompt
|
|
28
|
+
|
|
29
|
+
# The id of the model that generated the image.
|
|
30
|
+
attr_reader :model
|
|
31
|
+
|
|
32
|
+
# Generates an image from +prompt+ and returns an Image. Most code
|
|
33
|
+
# calls this through RubyLLM.paint.
|
|
34
|
+
#
|
|
35
|
+
# +model:+ selects the image model and defaults to the configured
|
|
36
|
+
# +default_image_model+. +provider:+ forces a specific provider, and
|
|
37
|
+
# +assume_model_exists:+ skips the registry lookup, which is useful
|
|
38
|
+
# for custom endpoints. +size:+ requests dimensions on models that
|
|
39
|
+
# support it. +count:+ asks for several images in one request, returning
|
|
40
|
+
# an Array of Images instead of one. +with:+ passes one or more source
|
|
41
|
+
# images for editing, and +mask:+ constrains which parts of the image
|
|
42
|
+
# may change. +provider_options:+ takes options in the provider's
|
|
43
|
+
# request vocabulary and merges them into the request as-is.
|
|
44
|
+
# +context:+ supplies a Context whose configuration replaces the
|
|
45
|
+
# global one. +metadata:+ is included in the instrumentation payload.
|
|
46
|
+
#
|
|
47
|
+
# image = RubyLLM.paint("A small watercolor robot", model: "gpt-image-2")
|
|
48
|
+
#
|
|
49
|
+
# images = RubyLLM.paint("A small watercolor robot", count: 4)
|
|
50
|
+
# images.each_with_index { |image, i| image.save("robot-#{i}.png") }
|
|
51
|
+
#
|
|
52
|
+
# RubyLLM.paint(
|
|
53
|
+
# "Turn the logo green and keep the background transparent",
|
|
54
|
+
# model: "gpt-image-2",
|
|
55
|
+
# with: "logo.png"
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# Providers that cannot generate several images in one request ignore
|
|
59
|
+
# +count:+ and return a single Image.
|
|
60
|
+
def self.paint(prompt,
|
|
61
|
+
model: nil,
|
|
62
|
+
provider: nil,
|
|
63
|
+
assume_model_exists: false,
|
|
64
|
+
size: nil,
|
|
65
|
+
count: nil,
|
|
66
|
+
context: nil,
|
|
67
|
+
with: nil,
|
|
68
|
+
mask: nil,
|
|
69
|
+
provider_options: {},
|
|
70
|
+
metadata: nil)
|
|
71
|
+
config = context&.config || RubyLLM.config
|
|
72
|
+
model ||= config.default_image_model
|
|
73
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
74
|
+
config: config)
|
|
75
|
+
empty_tokens = Tokens.new
|
|
76
|
+
payload = {
|
|
77
|
+
provider: provider_instance.slug,
|
|
78
|
+
provider_class: provider_instance.class.display_name,
|
|
79
|
+
model: model.id,
|
|
80
|
+
model_info: model,
|
|
81
|
+
prompt: prompt,
|
|
82
|
+
size: size,
|
|
83
|
+
count: count,
|
|
84
|
+
provider_options: provider_options,
|
|
85
|
+
metadata: metadata,
|
|
86
|
+
tokens: empty_tokens,
|
|
87
|
+
cost: Cost.new(tokens: empty_tokens, model:, category: :images)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
RubyLLM.instrument('image.ruby_llm', payload, config: config) do |event|
|
|
91
|
+
result = provider_instance.paint(prompt, model:, size:, count:, with:, mask:, provider_options:)
|
|
92
|
+
images = Support::Utils.to_safe_array(result)
|
|
93
|
+
event[:result] = result
|
|
94
|
+
event[:response_model] = images.first&.model
|
|
95
|
+
event[:tokens] = Tokens.aggregate(images.map(&:tokens))
|
|
96
|
+
event[:cost] = Cost.aggregate(images.map(&:cost))
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# :stopdoc:
|
|
102
|
+
|
|
103
|
+
# Set by the protocol that generated the image, so a Context's
|
|
104
|
+
# connection settings reach #to_blob.
|
|
105
|
+
attr_writer :config
|
|
106
|
+
|
|
107
|
+
def config
|
|
108
|
+
@config || RubyLLM.config
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model: nil, usage: {})
|
|
11
112
|
@url = url
|
|
12
113
|
@data = data
|
|
13
114
|
@mime_type = mime_type
|
|
14
115
|
@revised_prompt = revised_prompt
|
|
15
|
-
@
|
|
16
|
-
@
|
|
116
|
+
@model = model
|
|
117
|
+
@raw_usage = usage
|
|
17
118
|
end
|
|
119
|
+
# :startdoc:
|
|
18
120
|
|
|
121
|
+
# Returns +true+ if the image holds inline Base64 data, +false+ otherwise.
|
|
19
122
|
def base64?
|
|
20
123
|
!@data.nil?
|
|
21
124
|
end
|
|
22
125
|
|
|
126
|
+
# Returns the raw binary image bytes, decoding #data when present or
|
|
127
|
+
# downloading from #url otherwise.
|
|
128
|
+
#
|
|
129
|
+
# image_bytes = image.to_blob
|
|
130
|
+
#
|
|
23
131
|
def to_blob
|
|
24
132
|
if base64?
|
|
25
133
|
Base64.decode64 @data
|
|
26
134
|
else
|
|
27
|
-
response = Connection.basic.get @url
|
|
135
|
+
response = Transport::Connection.basic(config).get @url
|
|
28
136
|
response.body
|
|
29
137
|
end
|
|
30
138
|
end
|
|
31
139
|
|
|
140
|
+
# Writes the binary image to +path+, expanding it first. Returns
|
|
141
|
+
# +path+ as given.
|
|
142
|
+
#
|
|
143
|
+
# image.save("steampunk_owl.png")
|
|
144
|
+
#
|
|
32
145
|
def save(path)
|
|
33
146
|
File.binwrite(File.expand_path(path), to_blob)
|
|
34
147
|
path
|
|
35
148
|
end
|
|
36
149
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
with: nil,
|
|
44
|
-
mask: nil,
|
|
45
|
-
params: {})
|
|
46
|
-
config = context&.config || RubyLLM.config
|
|
47
|
-
model ||= config.default_image_model
|
|
48
|
-
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
49
|
-
config: config)
|
|
50
|
-
model_id = model.id
|
|
51
|
-
|
|
52
|
-
provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
|
|
53
|
-
end
|
|
54
|
-
|
|
150
|
+
# Returns a Tokens with usage across every provider attempt.
|
|
151
|
+
# Its fields are +nil+ when none were reported.
|
|
152
|
+
#
|
|
153
|
+
# image.tokens.input
|
|
154
|
+
# image.tokens.output
|
|
155
|
+
#
|
|
55
156
|
def tokens
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
157
|
+
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
158
|
+
|
|
159
|
+
@tokens ||= Tokens.new(
|
|
160
|
+
input: raw_usage['input_tokens'],
|
|
161
|
+
output: raw_usage['output_tokens'],
|
|
162
|
+
reported_cost: raw_usage['cost']
|
|
59
163
|
)
|
|
60
164
|
end
|
|
61
165
|
|
|
166
|
+
# Returns a Cost across every provider attempt, using reported prices
|
|
167
|
+
# when available and registry pricing otherwise.
|
|
168
|
+
#
|
|
169
|
+
# image.cost.total
|
|
170
|
+
#
|
|
62
171
|
def cost
|
|
172
|
+
return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
|
|
173
|
+
|
|
63
174
|
Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
|
|
64
175
|
end
|
|
65
176
|
|
|
177
|
+
# Returns the registry Model for #model, or +nil+ if the model id
|
|
178
|
+
# is missing or not in the registry.
|
|
66
179
|
def model_info
|
|
67
|
-
return unless
|
|
180
|
+
return unless model
|
|
68
181
|
|
|
69
|
-
@model_info ||= RubyLLM.models.find(
|
|
182
|
+
@model_info ||= RubyLLM.models.find(model)
|
|
70
183
|
rescue ModelNotFoundError
|
|
71
184
|
nil
|
|
72
185
|
end
|
|
73
186
|
|
|
74
187
|
private
|
|
75
188
|
|
|
189
|
+
attr_reader :raw_usage
|
|
190
|
+
|
|
76
191
|
def input_tokens_details
|
|
77
|
-
|
|
192
|
+
raw_usage['input_tokens_details']
|
|
78
193
|
end
|
|
79
194
|
|
|
80
|
-
def
|
|
81
|
-
|
|
195
|
+
def inspect_attributes # :nodoc:
|
|
196
|
+
{
|
|
197
|
+
model: model,
|
|
198
|
+
mime_type: mime_type,
|
|
199
|
+
url: url,
|
|
200
|
+
data: data && "#{data.bytesize} bytes",
|
|
201
|
+
revised_prompt: revised_prompt
|
|
202
|
+
}
|
|
82
203
|
end
|
|
83
204
|
end
|
|
84
205
|
end
|