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
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -1,82 +1,205 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
|
-
#
|
|
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
|
+
#
|
|
5
12
|
class Image
|
|
6
|
-
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
include Accounting::Usage::Result
|
|
15
|
+
|
|
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
|
|
7
100
|
|
|
8
|
-
|
|
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: {})
|
|
9
112
|
@url = url
|
|
10
113
|
@data = data
|
|
11
114
|
@mime_type = mime_type
|
|
12
115
|
@revised_prompt = revised_prompt
|
|
13
|
-
@
|
|
14
|
-
@
|
|
116
|
+
@model = model
|
|
117
|
+
@raw_usage = usage
|
|
15
118
|
end
|
|
119
|
+
# :startdoc:
|
|
16
120
|
|
|
121
|
+
# Returns +true+ if the image holds inline Base64 data, +false+ otherwise.
|
|
17
122
|
def base64?
|
|
18
123
|
!@data.nil?
|
|
19
124
|
end
|
|
20
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
|
+
#
|
|
21
131
|
def to_blob
|
|
22
132
|
if base64?
|
|
23
133
|
Base64.decode64 @data
|
|
24
134
|
else
|
|
25
|
-
response = Connection.basic.get @url
|
|
135
|
+
response = Transport::Connection.basic(config).get @url
|
|
26
136
|
response.body
|
|
27
137
|
end
|
|
28
138
|
end
|
|
29
139
|
|
|
140
|
+
# Writes the binary image to +path+, expanding it first. Returns
|
|
141
|
+
# +path+ as given.
|
|
142
|
+
#
|
|
143
|
+
# image.save("steampunk_owl.png")
|
|
144
|
+
#
|
|
30
145
|
def save(path)
|
|
31
146
|
File.binwrite(File.expand_path(path), to_blob)
|
|
32
147
|
path
|
|
33
148
|
end
|
|
34
149
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
with: nil,
|
|
42
|
-
mask: nil,
|
|
43
|
-
params: {})
|
|
44
|
-
config = context&.config || RubyLLM.config
|
|
45
|
-
model ||= config.default_image_model
|
|
46
|
-
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
47
|
-
config: config)
|
|
48
|
-
model_id = model.id
|
|
49
|
-
|
|
50
|
-
provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
|
|
51
|
-
end
|
|
52
|
-
|
|
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
|
+
#
|
|
53
156
|
def tokens
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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']
|
|
57
163
|
)
|
|
58
164
|
end
|
|
59
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
|
+
#
|
|
60
171
|
def cost
|
|
172
|
+
return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
|
|
173
|
+
|
|
61
174
|
Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
|
|
62
175
|
end
|
|
63
176
|
|
|
177
|
+
# Returns the registry Model for #model, or +nil+ if the model id
|
|
178
|
+
# is missing or not in the registry.
|
|
64
179
|
def model_info
|
|
65
|
-
return unless
|
|
180
|
+
return unless model
|
|
66
181
|
|
|
67
|
-
@model_info ||= RubyLLM.models.find(
|
|
182
|
+
@model_info ||= RubyLLM.models.find(model)
|
|
68
183
|
rescue ModelNotFoundError
|
|
69
184
|
nil
|
|
70
185
|
end
|
|
71
186
|
|
|
72
187
|
private
|
|
73
188
|
|
|
189
|
+
attr_reader :raw_usage
|
|
190
|
+
|
|
74
191
|
def input_tokens_details
|
|
75
|
-
|
|
192
|
+
raw_usage['input_tokens_details']
|
|
76
193
|
end
|
|
77
194
|
|
|
78
|
-
def
|
|
79
|
-
|
|
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
|
+
}
|
|
80
203
|
end
|
|
81
204
|
end
|
|
82
205
|
end
|
data/lib/ruby_llm/message.rb
CHANGED
|
@@ -1,127 +1,306 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
# A single
|
|
4
|
+
# A Message is a single entry in a chat conversation: a user prompt, an
|
|
5
|
+
# assistant reply, a system instruction, or a tool result. Chat#ask
|
|
6
|
+
# returns the model's reply as a Message, and Chat#messages holds the
|
|
7
|
+
# transcript as an array of them.
|
|
8
|
+
#
|
|
9
|
+
# response = chat.ask "What is the capital of France?"
|
|
10
|
+
# response.role # => :assistant
|
|
11
|
+
# response.content # => "The capital of France is Paris."
|
|
12
|
+
# response.finish_reason # => :stop
|
|
13
|
+
#
|
|
14
|
+
# A Message also carries everything else the provider returned: token
|
|
15
|
+
# usage (#tokens), reasoning output (#thinking), source citations
|
|
16
|
+
# (#citations), and requested tool calls (#tool_calls).
|
|
5
17
|
class Message
|
|
18
|
+
include Support::Inspectable
|
|
19
|
+
include Accounting::Usage::Result
|
|
20
|
+
|
|
21
|
+
# The valid message roles: +:system+, +:user+, +:assistant+, and +:tool+.
|
|
6
22
|
ROLES = %i[system user assistant tool].freeze
|
|
7
23
|
|
|
8
|
-
|
|
9
|
-
|
|
24
|
+
# The role of the message: +:system+, +:user+, +:assistant+, or +:tool+.
|
|
25
|
+
attr_reader :role
|
|
26
|
+
|
|
27
|
+
# The message text as a String. Empty for assistant messages that only
|
|
28
|
+
# request tool calls.
|
|
29
|
+
attr_reader :content
|
|
30
|
+
|
|
31
|
+
# The files sent or returned with the message, as an array of
|
|
32
|
+
# Attachment objects.
|
|
33
|
+
attr_reader :attachments
|
|
34
|
+
|
|
35
|
+
# The ID of the model that produced the message, +nil+ on user messages.
|
|
36
|
+
attr_reader :model
|
|
37
|
+
|
|
38
|
+
# The tool calls the assistant requested, as a Hash of ToolCall objects
|
|
39
|
+
# keyed by call ID, or +nil+.
|
|
40
|
+
attr_reader :tool_calls
|
|
41
|
+
|
|
42
|
+
# The ID of the tool call this message answers. Set only on tool result
|
|
43
|
+
# messages.
|
|
44
|
+
attr_reader :tool_call_id
|
|
45
|
+
|
|
46
|
+
# The raw provider response: a Faraday::Response, or the result body
|
|
47
|
+
# Hash for messages retrieved from a Batch.
|
|
48
|
+
attr_reader :raw
|
|
49
|
+
|
|
50
|
+
# The model's reasoning output as a Thinking object, or +nil+ when the
|
|
51
|
+
# provider returned none.
|
|
52
|
+
attr_reader :thinking
|
|
53
|
+
|
|
54
|
+
# The source citations as an array of Citation objects, normalized
|
|
55
|
+
# across providers.
|
|
56
|
+
attr_reader :citations
|
|
57
|
+
|
|
58
|
+
# Why the model stopped: +:stop+, +:max_tokens+, +:tool_calls+, or
|
|
59
|
+
# +:content_filter+. Any other reason comes through as the provider
|
|
60
|
+
# spelled it, such as Anthropic's +:pause_turn+.
|
|
61
|
+
attr_reader :finish_reason
|
|
62
|
+
|
|
63
|
+
# The provider-executed tool steps in this response, as an array of
|
|
64
|
+
# ServerToolCall objects. Empty unless the chat enabled tools with
|
|
65
|
+
# Chat#with_server_tools and the model used one.
|
|
66
|
+
attr_reader :server_tool_calls
|
|
67
|
+
|
|
68
|
+
# The provider-shaped content blocks of this assistant message, kept
|
|
69
|
+
# verbatim when the response used server tools so later requests can
|
|
70
|
+
# replay the turn exactly. +nil+ otherwise.
|
|
71
|
+
attr_reader :raw_content # :nodoc:
|
|
10
72
|
|
|
11
|
-
|
|
73
|
+
# The provider-shaped reasoning payload of this assistant message, kept
|
|
74
|
+
# verbatim so later requests can replay the model's reasoning exactly.
|
|
75
|
+
# +nil+ when the provider returned none.
|
|
76
|
+
attr_reader :raw_reasoning # :nodoc:
|
|
77
|
+
|
|
78
|
+
# The Chat this message belongs to, set when it is added to a
|
|
79
|
+
# conversation. Backs #tool_results.
|
|
80
|
+
attr_accessor :conversation # :nodoc:
|
|
81
|
+
|
|
82
|
+
def initialize(options = {}) # :nodoc:
|
|
12
83
|
@role = options.fetch(:role).to_sym
|
|
13
|
-
@tool_calls = options[:tool_calls]
|
|
14
|
-
@content = normalize_content(options.fetch(:content)
|
|
15
|
-
@
|
|
84
|
+
@tool_calls = coerce_tool_calls(options[:tool_calls])
|
|
85
|
+
@content = normalize_content(options.fetch(:content))
|
|
86
|
+
@config = options[:config]
|
|
87
|
+
@attachments = Attachment.wrap(options[:attachments], config: @config)
|
|
88
|
+
@model = options[:model]
|
|
89
|
+
@supplied_cost = coerce_value(options[:cost], Cost)
|
|
16
90
|
@tool_call_id = options[:tool_call_id]
|
|
17
|
-
@tokens = options[:tokens] || Tokens.
|
|
91
|
+
@tokens = options[:tokens] || Tokens.new(
|
|
18
92
|
input: options[:input_tokens],
|
|
19
93
|
output: options[:output_tokens],
|
|
20
|
-
|
|
21
|
-
|
|
94
|
+
cache_read: options[:cache_read_tokens],
|
|
95
|
+
cache_write: options[:cache_write_tokens],
|
|
22
96
|
thinking: options[:thinking_tokens],
|
|
23
|
-
|
|
97
|
+
server_tool_use: options[:server_tool_use],
|
|
98
|
+
reported_cost: options[:reported_cost]
|
|
24
99
|
)
|
|
25
100
|
@raw = options[:raw]
|
|
26
|
-
@thinking = options[:thinking]
|
|
101
|
+
@thinking = coerce_thinking(options[:thinking], options[:thinking_signature])
|
|
102
|
+
@citations = Array(options[:citations]).map { |citation| coerce_value(citation, Citation) }
|
|
103
|
+
@server_tool_calls = Array(options[:server_tool_calls]).map { |call| coerce_value(call, ServerToolCall) }
|
|
104
|
+
@raw_content = options[:raw_content]
|
|
105
|
+
@raw_reasoning = options[:raw_reasoning]
|
|
106
|
+
@finish_reason = options[:finish_reason]&.to_sym
|
|
107
|
+
self.ruby_llm_usage_entries = options[:usage_entries] if options[:usage_entries]
|
|
108
|
+
@cache_until_here = options.fetch(:cache_until_here, false)
|
|
27
109
|
|
|
28
110
|
ensure_valid_role
|
|
29
111
|
end
|
|
30
112
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
# Returns #content parsed as JSON, memoized after the first call.
|
|
114
|
+
# Useful for reading structured output responses.
|
|
115
|
+
#
|
|
116
|
+
# response = chat.with_schema(PersonSchema).ask "Generate a person"
|
|
117
|
+
# response.parsed # => {"name" => "Alice", "age" => 30}
|
|
118
|
+
#
|
|
119
|
+
def parsed
|
|
120
|
+
return if content.nil? || content.empty?
|
|
121
|
+
|
|
122
|
+
@parsed ||= JSON.parse(content)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def with_attachments(attachments) # :nodoc:
|
|
126
|
+
wrapped = Attachment.wrap(attachments, config: @config)
|
|
127
|
+
dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
|
|
37
128
|
end
|
|
38
129
|
|
|
130
|
+
# Returns +true+ if the assistant requested one or more tool calls,
|
|
131
|
+
# +false+ otherwise.
|
|
39
132
|
def tool_call?
|
|
40
133
|
!tool_calls.nil? && !tool_calls.empty?
|
|
41
134
|
end
|
|
42
135
|
|
|
136
|
+
# Returns +true+ if the message carries the result of a tool call,
|
|
137
|
+
# +false+ otherwise.
|
|
43
138
|
def tool_result?
|
|
44
139
|
!tool_call_id.nil? && !tool_call_id.empty?
|
|
45
140
|
end
|
|
46
141
|
|
|
142
|
+
# Returns the tool result messages answering this message's tool calls,
|
|
143
|
+
# or an empty array when it made none. Mirrors the +tool_results+
|
|
144
|
+
# association on acts_as_message records.
|
|
47
145
|
def tool_results
|
|
48
|
-
|
|
49
|
-
end
|
|
146
|
+
return [] unless tool_call? && conversation
|
|
50
147
|
|
|
51
|
-
|
|
52
|
-
|
|
148
|
+
conversation.messages.select do |message|
|
|
149
|
+
message.tool_result? && tool_calls.key?(message.tool_call_id)
|
|
150
|
+
end
|
|
53
151
|
end
|
|
54
152
|
|
|
55
|
-
|
|
56
|
-
|
|
153
|
+
# Returns +true+ if #finish_reason indicates the model finished
|
|
154
|
+
# normally, +false+ otherwise. A turn that stopped to call tools is
|
|
155
|
+
# reported by #tool_call_stop? instead, whatever the provider named it.
|
|
156
|
+
def stopped?
|
|
157
|
+
finish_reason == :stop && !tool_call?
|
|
57
158
|
end
|
|
58
159
|
|
|
59
|
-
|
|
60
|
-
|
|
160
|
+
# Returns +true+ if the response was cut off by a token limit,
|
|
161
|
+
# +false+ otherwise.
|
|
162
|
+
def max_tokens?
|
|
163
|
+
finish_reason == :max_tokens
|
|
61
164
|
end
|
|
62
165
|
|
|
63
|
-
|
|
64
|
-
|
|
166
|
+
# Returns +true+ if the model stopped to request tool calls,
|
|
167
|
+
# +false+ otherwise.
|
|
168
|
+
def tool_call_stop?
|
|
169
|
+
finish_reason == :tool_calls || (tool_call? && finish_reason == :stop)
|
|
65
170
|
end
|
|
66
171
|
|
|
67
|
-
|
|
68
|
-
|
|
172
|
+
# Returns +true+ if a provider safety filter stopped the response,
|
|
173
|
+
# +false+ otherwise.
|
|
174
|
+
def content_filtered?
|
|
175
|
+
finish_reason == :content_filter
|
|
69
176
|
end
|
|
70
177
|
|
|
71
|
-
|
|
72
|
-
|
|
178
|
+
# Returns usage aggregated across every provider attempt that produced this
|
|
179
|
+
# message. Messages constructed by hand report the token counts they were
|
|
180
|
+
# built with.
|
|
181
|
+
def tokens
|
|
182
|
+
return @tokens if ruby_llm_usage_entries.empty?
|
|
183
|
+
|
|
184
|
+
ruby_llm_usage_tokens
|
|
73
185
|
end
|
|
74
186
|
|
|
75
|
-
|
|
76
|
-
|
|
187
|
+
# Returns a Cost pricing this message's token usage in US dollars.
|
|
188
|
+
# Uses recorded attempt costs, an explicitly supplied +cost:+, or pricing
|
|
189
|
+
# from #model_info. An explicit +model:+ overrides those costs for repricing.
|
|
190
|
+
#
|
|
191
|
+
# response.cost.total
|
|
192
|
+
#
|
|
193
|
+
def cost(model: nil)
|
|
194
|
+
return ruby_llm_usage_cost if model.nil? && ruby_llm_usage_entries.any?
|
|
195
|
+
return @supplied_cost if model.nil? && @supplied_cost
|
|
196
|
+
|
|
197
|
+
Cost.new(tokens:, model: model || model_info)
|
|
77
198
|
end
|
|
78
199
|
|
|
79
|
-
|
|
80
|
-
|
|
200
|
+
# Marks this message as an explicit prompt cache boundary. Providers
|
|
201
|
+
# with boundary controls use the conversation up to and including this
|
|
202
|
+
# message as the cacheable prefix. Returns +self+.
|
|
203
|
+
#
|
|
204
|
+
# chat.add_message(role: :user, content: long_context).cache_until_here
|
|
205
|
+
#
|
|
206
|
+
def cache_until_here
|
|
207
|
+
@cache_until_here = true
|
|
208
|
+
self
|
|
81
209
|
end
|
|
82
210
|
|
|
83
|
-
|
|
84
|
-
|
|
211
|
+
# Returns +true+ if the message carries an explicit prompt cache
|
|
212
|
+
# boundary, +false+ otherwise.
|
|
213
|
+
def cache_until_here?
|
|
214
|
+
@cache_until_here
|
|
85
215
|
end
|
|
86
216
|
|
|
217
|
+
# Returns a Hash of the message's attributes, with token counts merged
|
|
218
|
+
# in as +:input_tokens+, +:output_tokens+, and related keys. Omits
|
|
219
|
+
# +nil+ values and empty attachment and citation lists. Includes +:cost+
|
|
220
|
+
# only when supplied explicitly, preserving unknown costs on round-trip.
|
|
87
221
|
def to_h
|
|
88
222
|
{
|
|
89
223
|
role: role,
|
|
90
224
|
content: content,
|
|
91
|
-
|
|
92
|
-
|
|
225
|
+
attachments: list_to_h(attachments),
|
|
226
|
+
model: model,
|
|
227
|
+
cost: @supplied_cost && cost.to_h,
|
|
228
|
+
tool_calls: tool_calls&.transform_values(&:to_h),
|
|
93
229
|
tool_call_id: tool_call_id,
|
|
94
230
|
thinking: thinking&.text,
|
|
95
|
-
thinking_signature: thinking&.signature
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
231
|
+
thinking_signature: thinking&.signature,
|
|
232
|
+
citations: list_to_h(citations),
|
|
233
|
+
server_tool_calls: list_to_h(server_tool_calls),
|
|
234
|
+
raw_content: raw_content,
|
|
235
|
+
raw_reasoning: raw_reasoning,
|
|
236
|
+
finish_reason: finish_reason,
|
|
237
|
+
cache_until_here: cache_until_here? || nil
|
|
238
|
+
}.merge(tokens.to_h).compact
|
|
101
239
|
end
|
|
102
240
|
|
|
241
|
+
# Returns the Model record for #model from the model registry, or
|
|
242
|
+
# +nil+ when the message has no model or the model is unknown.
|
|
103
243
|
def model_info
|
|
104
|
-
return unless
|
|
244
|
+
return unless model
|
|
105
245
|
|
|
106
|
-
@model_info ||= RubyLLM.models.find(
|
|
246
|
+
@model_info ||= RubyLLM.models.find(model)
|
|
107
247
|
rescue ModelNotFoundError
|
|
108
248
|
nil
|
|
109
249
|
end
|
|
110
250
|
|
|
111
251
|
private
|
|
112
252
|
|
|
113
|
-
def
|
|
114
|
-
|
|
253
|
+
def list_to_h(list)
|
|
254
|
+
list.empty? ? nil : list.map(&:to_h)
|
|
255
|
+
end
|
|
115
256
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
257
|
+
def coerce_tool_calls(tool_calls)
|
|
258
|
+
return tool_calls unless tool_calls.is_a?(Hash)
|
|
259
|
+
|
|
260
|
+
tool_calls.to_h do |id, call|
|
|
261
|
+
next [id, call] unless call.is_a?(Hash)
|
|
262
|
+
|
|
263
|
+
attributes = call.transform_keys(&:to_sym)
|
|
264
|
+
[id, ToolCall.new(id: attributes[:id] || id, name: attributes[:name],
|
|
265
|
+
arguments: attributes[:arguments] || {},
|
|
266
|
+
thought_signature: attributes[:thought_signature], remote: attributes.fetch(:remote, false))]
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def coerce_thinking(thinking, signature)
|
|
271
|
+
case thinking
|
|
272
|
+
when nil, Thinking then thinking
|
|
273
|
+
when Hash then Thinking.build(**thinking.transform_keys(&:to_sym).slice(:text, :signature))
|
|
274
|
+
else Thinking.build(text: thinking.to_s, signature: signature)
|
|
120
275
|
end
|
|
121
276
|
end
|
|
122
277
|
|
|
278
|
+
def coerce_value(value, klass)
|
|
279
|
+
value.is_a?(Hash) ? klass.from_h(value) : value
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def normalize_content(content)
|
|
283
|
+
return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
|
|
284
|
+
return content if content.nil? || content.is_a?(String)
|
|
285
|
+
|
|
286
|
+
raise ArgumentError,
|
|
287
|
+
"Message content must be a String, got #{content.class}. " \
|
|
288
|
+
'Pass files via attachments: and structured data as JSON.'
|
|
289
|
+
end
|
|
290
|
+
|
|
123
291
|
def ensure_valid_role
|
|
124
292
|
raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
|
|
125
293
|
end
|
|
294
|
+
|
|
295
|
+
def inspect_attributes # :nodoc:
|
|
296
|
+
{
|
|
297
|
+
role: role,
|
|
298
|
+
content: content,
|
|
299
|
+
tool_calls: tool_calls&.values&.map(&:name),
|
|
300
|
+
tool_call_id: tool_call_id,
|
|
301
|
+
model: model,
|
|
302
|
+
finish_reason: finish_reason
|
|
303
|
+
}
|
|
304
|
+
end
|
|
126
305
|
end
|
|
127
306
|
end
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
|
|
5
|
-
#
|
|
4
|
+
class Model
|
|
5
|
+
# A Model::Modalities lists the kinds of content a model accepts and
|
|
6
|
+
# produces, as arrays of Strings. Instances come from Model#modalities.
|
|
7
|
+
#
|
|
8
|
+
# model = RubyLLM.models.find('gpt-5.6')
|
|
9
|
+
# model.modalities.input # => ["text", "image", "pdf"]
|
|
10
|
+
# model.modalities.output # => ["text"]
|
|
11
|
+
#
|
|
6
12
|
class Modalities
|
|
7
|
-
|
|
13
|
+
# The input modalities as an array of Strings,
|
|
14
|
+
# e.g. <tt>["text", "image", "pdf"]</tt>.
|
|
15
|
+
attr_reader :input
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
# The output modalities as an array of Strings,
|
|
18
|
+
# e.g. <tt>["text"]</tt> or <tt>["embeddings"]</tt>.
|
|
19
|
+
attr_reader :output
|
|
20
|
+
|
|
21
|
+
def initialize(data) # :nodoc:
|
|
10
22
|
@input = Array(data[:input]).map(&:to_s)
|
|
11
23
|
@output = Array(data[:output]).map(&:to_s)
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
# Returns the modalities as a Hash with +:input+ and +:output+ keys.
|
|
14
27
|
def to_h
|
|
15
28
|
{
|
|
16
29
|
input: input,
|