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/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,
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
|
|
5
|
-
# A
|
|
4
|
+
class Model
|
|
5
|
+
# A Pricing groups a model's prices by usage category: text tokens,
|
|
6
|
+
# images, audio tokens, and embeddings. Each category is a
|
|
7
|
+
# PricingCategory. Prices are in USD per million tokens. Instances come
|
|
8
|
+
# from Model#pricing.
|
|
9
|
+
#
|
|
10
|
+
# model = RubyLLM.models.find "claude-sonnet-5"
|
|
11
|
+
# model.pricing.text_tokens.input # => 3
|
|
12
|
+
# model.pricing.text_tokens.output # => 15
|
|
13
|
+
#
|
|
6
14
|
class Pricing
|
|
15
|
+
# The pricing categories a model may define.
|
|
7
16
|
CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
|
|
8
17
|
|
|
9
|
-
def initialize(data)
|
|
18
|
+
def initialize(data) # :nodoc:
|
|
10
19
|
@data = {}
|
|
11
20
|
|
|
12
21
|
CATEGORIES.each do |category|
|
|
@@ -14,22 +23,32 @@ module RubyLLM
|
|
|
14
23
|
end
|
|
15
24
|
end
|
|
16
25
|
|
|
26
|
+
# Returns the PricingCategory for text token prices, or an empty
|
|
27
|
+
# category if the model has none.
|
|
17
28
|
def text_tokens
|
|
18
29
|
category(:text_tokens)
|
|
19
30
|
end
|
|
20
31
|
|
|
32
|
+
# Returns the PricingCategory for image generation prices, or an empty
|
|
33
|
+
# category if the model has none.
|
|
21
34
|
def images
|
|
22
35
|
category(:images)
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
# Returns the PricingCategory for audio token prices, or an empty
|
|
39
|
+
# category if the model has none.
|
|
25
40
|
def audio_tokens
|
|
26
41
|
category(:audio_tokens)
|
|
27
42
|
end
|
|
28
43
|
|
|
44
|
+
# Returns the PricingCategory for embedding prices, or an empty
|
|
45
|
+
# category if the model has none.
|
|
29
46
|
def embeddings
|
|
30
47
|
category(:embeddings)
|
|
31
48
|
end
|
|
32
49
|
|
|
50
|
+
# Returns the pricing data as a nested Hash keyed by category.
|
|
51
|
+
# Categories without prices are omitted.
|
|
33
52
|
def to_h
|
|
34
53
|
@data.transform_values(&:to_h)
|
|
35
54
|
end
|
|
@@ -43,11 +62,11 @@ module RubyLLM
|
|
|
43
62
|
def empty_pricing?(data)
|
|
44
63
|
return true unless data
|
|
45
64
|
|
|
46
|
-
|
|
65
|
+
PricingCategory::TIERS.each do |tier|
|
|
47
66
|
next unless data[tier]
|
|
48
67
|
|
|
49
68
|
data[tier].each_value do |value|
|
|
50
|
-
return false
|
|
69
|
+
return false unless value.nil?
|
|
51
70
|
end
|
|
52
71
|
end
|
|
53
72
|
|
|
@@ -1,56 +1,145 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
|
|
5
|
-
#
|
|
4
|
+
class Model
|
|
5
|
+
# A PricingCategory holds the standard, batch, and long-context pricing
|
|
6
|
+
# tiers for one kind of model usage, such as text tokens or images.
|
|
7
|
+
# Model#pricing returns a Pricing collection whose categories are
|
|
8
|
+
# PricingCategory instances. Prices are in USD per million tokens.
|
|
9
|
+
#
|
|
10
|
+
# model = RubyLLM.models.find "claude-sonnet-5"
|
|
11
|
+
# category = model.pricing.text_tokens
|
|
12
|
+
# category.input # => 3
|
|
13
|
+
# category.output # => 15
|
|
14
|
+
#
|
|
6
15
|
class PricingCategory
|
|
7
|
-
|
|
16
|
+
# The billing tiers a category may define.
|
|
17
|
+
TIERS = %i[standard batch long_context].freeze
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
# The standard-tier PricingTier, or +nil+ when the model has no
|
|
20
|
+
# standard pricing for this category.
|
|
21
|
+
attr_reader :standard
|
|
22
|
+
|
|
23
|
+
# The batch-tier PricingTier, or +nil+ when the model has no batch
|
|
24
|
+
# pricing for this category.
|
|
25
|
+
attr_reader :batch
|
|
26
|
+
|
|
27
|
+
# The long-context PricingTier, or +nil+ when the model has no
|
|
28
|
+
# separate rates for prompts above #long_context_threshold.
|
|
29
|
+
attr_reader :long_context
|
|
30
|
+
|
|
31
|
+
# Prompt-size threshold (in tokens) above which long-context rates
|
|
32
|
+
# apply, or +nil+ when the model has no long-context tier.
|
|
33
|
+
attr_reader :long_context_threshold
|
|
34
|
+
|
|
35
|
+
def initialize(data = {}) # :nodoc:
|
|
36
|
+
data = data.transform_keys(&:to_sym) if data.respond_to?(:transform_keys)
|
|
37
|
+
|
|
38
|
+
@standard = tier_from(data[:standard])
|
|
39
|
+
@batch = tier_from(data[:batch])
|
|
40
|
+
@long_context = tier_from(data[:long_context])
|
|
41
|
+
@long_context_threshold = Integer(data[:long_context_threshold], exception: false)
|
|
12
42
|
end
|
|
13
43
|
|
|
44
|
+
# Returns the standard-tier input price in USD per million tokens,
|
|
45
|
+
# or +nil+ if the price is missing.
|
|
14
46
|
def input
|
|
15
47
|
standard&.input_per_million
|
|
16
48
|
end
|
|
17
49
|
|
|
50
|
+
# Returns the standard-tier output price in USD per million tokens,
|
|
51
|
+
# or +nil+ if the price is missing.
|
|
18
52
|
def output
|
|
19
53
|
standard&.output_per_million
|
|
20
54
|
end
|
|
21
55
|
|
|
56
|
+
# Returns the standard-tier cache read price in USD per million
|
|
57
|
+
# tokens, or +nil+ if the price is missing.
|
|
22
58
|
def cache_read_input
|
|
23
|
-
standard&.cache_read_input_per_million
|
|
59
|
+
standard&.cache_read_input_per_million
|
|
24
60
|
end
|
|
25
61
|
|
|
62
|
+
# Returns the standard-tier cache write price in USD per million
|
|
63
|
+
# tokens, or +nil+ if the price is missing.
|
|
26
64
|
def cache_write_input
|
|
27
|
-
standard&.cache_write_input_per_million
|
|
65
|
+
standard&.cache_write_input_per_million
|
|
28
66
|
end
|
|
29
67
|
|
|
68
|
+
# Returns the standard-tier reasoning output price in USD per million
|
|
69
|
+
# tokens, or +nil+ if the price is missing.
|
|
30
70
|
def reasoning_output
|
|
31
71
|
standard&.reasoning_output_per_million
|
|
32
72
|
end
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def
|
|
38
|
-
|
|
74
|
+
# Returns the PricingTier that applies for a prompt of the given size.
|
|
75
|
+
# Uses #long_context when that tier exists and +prompt_tokens+ is
|
|
76
|
+
# greater than #long_context_threshold; otherwise returns #standard.
|
|
77
|
+
def tier_for(prompt_tokens)
|
|
78
|
+
if long_context && long_context_threshold &&
|
|
79
|
+
prompt_tokens.to_i > long_context_threshold
|
|
80
|
+
long_context
|
|
81
|
+
else
|
|
82
|
+
standard
|
|
83
|
+
end
|
|
39
84
|
end
|
|
40
85
|
|
|
86
|
+
# Returns a Hash with present tier hashes and optional
|
|
87
|
+
# +:long_context_threshold+, omitting absent entries.
|
|
41
88
|
def to_h
|
|
42
89
|
result = {}
|
|
43
90
|
result[:standard] = standard.to_h if standard
|
|
44
91
|
result[:batch] = batch.to_h if batch
|
|
92
|
+
result[:long_context] = long_context.to_h if long_context
|
|
93
|
+
result[:long_context_threshold] = long_context_threshold if long_context_threshold
|
|
45
94
|
result
|
|
46
95
|
end
|
|
47
96
|
|
|
97
|
+
# Builds long-context rates and threshold from a models.dev-style cost
|
|
98
|
+
# Hash (as stored on Model#metadata under +:cost+). Returns
|
|
99
|
+
# <tt>[rates_hash, threshold]</tt>, or <tt>[nil, nil]</tt> when the
|
|
100
|
+
# cost has no context tier.
|
|
101
|
+
def self.long_context_from_cost(cost) # :nodoc:
|
|
102
|
+
cost = RubyLLM::Support::Utils.deep_symbolize_keys(cost || {})
|
|
103
|
+
return [nil, nil] if cost.empty?
|
|
104
|
+
|
|
105
|
+
entry, threshold = context_cost(cost)
|
|
106
|
+
return [nil, nil] unless entry
|
|
107
|
+
|
|
108
|
+
rates = {
|
|
109
|
+
input_per_million: entry[:input],
|
|
110
|
+
output_per_million: entry[:output],
|
|
111
|
+
cache_read_input_per_million: entry[:cache_read],
|
|
112
|
+
cache_write_input_per_million: entry[:cache_write],
|
|
113
|
+
reasoning_output_per_million: entry[:reasoning]
|
|
114
|
+
}.compact
|
|
115
|
+
rates.empty? ? [nil, nil] : [rates, threshold]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.context_cost(cost) # :nodoc:
|
|
119
|
+
context_tier = Array(cost[:tiers]).find do |entry|
|
|
120
|
+
entry.is_a?(Hash) && entry.dig(:tier, :type).to_s == 'context'
|
|
121
|
+
end
|
|
122
|
+
if context_tier
|
|
123
|
+
[context_tier, Integer(context_tier.dig(:tier, :size), exception: false)]
|
|
124
|
+
elsif cost[:context_over_200k].is_a?(Hash)
|
|
125
|
+
[cost[:context_over_200k], 200_000]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private_class_method :context_cost
|
|
130
|
+
|
|
48
131
|
private
|
|
49
132
|
|
|
133
|
+
def tier_from(tier_data)
|
|
134
|
+
return nil if empty_tier?(tier_data)
|
|
135
|
+
|
|
136
|
+
PricingTier.new(tier_data || {})
|
|
137
|
+
end
|
|
138
|
+
|
|
50
139
|
def empty_tier?(tier_data)
|
|
51
140
|
return true unless tier_data
|
|
52
141
|
|
|
53
|
-
tier_data.values.all?
|
|
142
|
+
tier_data.values.all?(&:nil?)
|
|
54
143
|
end
|
|
55
144
|
end
|
|
56
145
|
end
|