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
|
@@ -1,43 +1,72 @@
|
|
|
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
|
|
7
|
-
|
|
15
|
+
# The pricing categories a model may define.
|
|
16
|
+
CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(data) # :nodoc:
|
|
8
19
|
@data = {}
|
|
9
20
|
|
|
10
|
-
|
|
21
|
+
CATEGORIES.each do |category|
|
|
11
22
|
@data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
|
|
12
23
|
end
|
|
13
24
|
end
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
# Returns the PricingCategory for text token prices, or an empty
|
|
27
|
+
# category if the model has none.
|
|
28
|
+
def text_tokens
|
|
29
|
+
category(:text_tokens)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the PricingCategory for image generation prices, or an empty
|
|
33
|
+
# category if the model has none.
|
|
34
|
+
def images
|
|
35
|
+
category(:images)
|
|
21
36
|
end
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
# Returns the PricingCategory for audio token prices, or an empty
|
|
39
|
+
# category if the model has none.
|
|
40
|
+
def audio_tokens
|
|
41
|
+
category(:audio_tokens)
|
|
25
42
|
end
|
|
26
43
|
|
|
44
|
+
# Returns the PricingCategory for embedding prices, or an empty
|
|
45
|
+
# category if the model has none.
|
|
46
|
+
def embeddings
|
|
47
|
+
category(:embeddings)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the pricing data as a nested Hash keyed by category.
|
|
51
|
+
# Categories without prices are omitted.
|
|
27
52
|
def to_h
|
|
28
53
|
@data.transform_values(&:to_h)
|
|
29
54
|
end
|
|
30
55
|
|
|
31
56
|
private
|
|
32
57
|
|
|
58
|
+
def category(name)
|
|
59
|
+
@data[name] || PricingCategory.new
|
|
60
|
+
end
|
|
61
|
+
|
|
33
62
|
def empty_pricing?(data)
|
|
34
63
|
return true unless data
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
PricingCategory::TIERS.each do |tier|
|
|
37
66
|
next unless data[tier]
|
|
38
67
|
|
|
39
68
|
data[tier].each_value do |value|
|
|
40
|
-
return false
|
|
69
|
+
return false unless value.nil?
|
|
41
70
|
end
|
|
42
71
|
end
|
|
43
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
|
|
@@ -1,32 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
|
|
5
|
-
# A
|
|
4
|
+
class Model
|
|
5
|
+
# A PricingTier holds the prices a model charges within one billing tier,
|
|
6
|
+
# such as standard, batch, or long_context. Each price is in USD per one
|
|
7
|
+
# million tokens. Prices missing from the registry read as +nil+. A zero
|
|
8
|
+
# price means the usage is free.
|
|
9
|
+
#
|
|
10
|
+
# Instances come from PricingCategory#standard, #batch, and #long_context:
|
|
11
|
+
#
|
|
12
|
+
# model = RubyLLM.models.find "claude-sonnet-5"
|
|
13
|
+
# tier = model.pricing.text_tokens.standard
|
|
14
|
+
# tier.input_per_million # => 3
|
|
15
|
+
# tier.output_per_million # => 15
|
|
16
|
+
#
|
|
6
17
|
class PricingTier
|
|
7
|
-
|
|
18
|
+
# The names of the price readers a tier responds to.
|
|
19
|
+
ATTRIBUTES = %i[
|
|
20
|
+
input_per_million
|
|
21
|
+
output_per_million
|
|
22
|
+
cache_read_input_per_million
|
|
23
|
+
cache_write_input_per_million
|
|
24
|
+
reasoning_output_per_million
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(data = {}) # :nodoc:
|
|
8
28
|
@values = {}
|
|
9
29
|
|
|
10
30
|
data.each do |key, value|
|
|
11
|
-
|
|
12
|
-
end
|
|
13
|
-
end
|
|
31
|
+
next if value.nil?
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
if method.to_s.end_with?('=')
|
|
17
|
-
key = method.to_s.chomp('=').to_sym
|
|
18
|
-
@values[key] = args.first if args.first && args.first != 0.0
|
|
19
|
-
elsif @values.key?(method)
|
|
20
|
-
@values[method]
|
|
33
|
+
@values[key.to_sym] = value
|
|
21
34
|
end
|
|
22
35
|
end
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
##
|
|
38
|
+
# :method: input_per_million
|
|
39
|
+
#
|
|
40
|
+
# Returns the USD price per million input tokens, or +nil+ if the
|
|
41
|
+
# price is missing.
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# :method: output_per_million
|
|
45
|
+
#
|
|
46
|
+
# Returns the USD price per million output tokens, or +nil+ if the
|
|
47
|
+
# price is missing.
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# :method: cache_read_input_per_million
|
|
51
|
+
#
|
|
52
|
+
# Returns the USD price per million input tokens read from the
|
|
53
|
+
# provider's prompt cache, or +nil+ if the price is missing.
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# :method: cache_write_input_per_million
|
|
57
|
+
#
|
|
58
|
+
# Returns the USD price per million input tokens written to the
|
|
59
|
+
# provider's prompt cache, or +nil+ if the price is missing.
|
|
60
|
+
|
|
61
|
+
##
|
|
62
|
+
# :method: reasoning_output_per_million
|
|
63
|
+
#
|
|
64
|
+
# Returns the USD price per million reasoning output tokens, or +nil+
|
|
65
|
+
# if the price is missing.
|
|
66
|
+
|
|
67
|
+
ATTRIBUTES.each do |attribute|
|
|
68
|
+
define_method(attribute) do
|
|
69
|
+
@values[attribute]
|
|
70
|
+
end
|
|
26
71
|
end
|
|
27
72
|
|
|
73
|
+
# Returns a new Hash of the tier's prices with Symbol keys. Missing
|
|
74
|
+
# prices are omitted.
|
|
75
|
+
#
|
|
76
|
+
# tier.to_h
|
|
77
|
+
# # => {input_per_million: 3, output_per_million: 15, ...}
|
|
78
|
+
#
|
|
28
79
|
def to_h
|
|
29
|
-
@values
|
|
80
|
+
@values.dup
|
|
30
81
|
end
|
|
31
82
|
end
|
|
32
83
|
end
|
data/lib/ruby_llm/model.rb
CHANGED
|
@@ -1,7 +1,249 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
# Model
|
|
5
|
-
|
|
4
|
+
# A Model describes one entry in the model registry: the model's identity,
|
|
5
|
+
# capabilities, modalities, pricing, and provider metadata. Instances come
|
|
6
|
+
# from the registry through RubyLLM.models and from Chat#model.
|
|
7
|
+
#
|
|
8
|
+
# model = RubyLLM.models.find('gpt-5.6')
|
|
9
|
+
# model.name # => "GPT-5.6"
|
|
10
|
+
# model.provider # => "openai"
|
|
11
|
+
# model.context_window # => 1050000
|
|
12
|
+
# model.supports?(:vision) # => true
|
|
13
|
+
#
|
|
14
|
+
class Model
|
|
15
|
+
include Support::Inspectable
|
|
16
|
+
|
|
17
|
+
# The provider's identifier for the model, e.g. <tt>"gpt-5.6"</tt>.
|
|
18
|
+
attr_reader :id
|
|
19
|
+
|
|
20
|
+
# The human-readable model name, e.g. <tt>"GPT-5.6"</tt>.
|
|
21
|
+
attr_reader :name
|
|
22
|
+
|
|
23
|
+
# The provider slug as a String, e.g. <tt>"openai"</tt>.
|
|
24
|
+
attr_reader :provider
|
|
25
|
+
|
|
26
|
+
# The model family as a String, e.g. <tt>"gpt"</tt>, or +nil+.
|
|
27
|
+
attr_reader :family
|
|
28
|
+
|
|
29
|
+
# The model's release timestamp as a UTC Time, or +nil+.
|
|
30
|
+
attr_reader :created_at
|
|
31
|
+
|
|
32
|
+
# The maximum number of input tokens the model accepts, or +nil+.
|
|
33
|
+
attr_reader :context_window
|
|
34
|
+
|
|
35
|
+
# The maximum number of tokens the model can generate, or +nil+.
|
|
36
|
+
attr_reader :max_output_tokens
|
|
37
|
+
|
|
38
|
+
# The model's training data cutoff as a Date, or +nil+.
|
|
39
|
+
attr_reader :knowledge_cutoff
|
|
40
|
+
|
|
41
|
+
# The supported input and output modalities as a Model::Modalities object.
|
|
42
|
+
#
|
|
43
|
+
# model.modalities.input # => ["text", "image", "pdf"]
|
|
44
|
+
# model.modalities.output # => ["text"]
|
|
45
|
+
#
|
|
46
|
+
attr_reader :modalities
|
|
47
|
+
|
|
48
|
+
# The model's capability names as an array of Strings,
|
|
49
|
+
# e.g. <tt>["function_calling", "streaming"]</tt>.
|
|
50
|
+
attr_reader :capabilities
|
|
51
|
+
|
|
52
|
+
# The model's pricing as a Model::Pricing object, in USD per million tokens.
|
|
53
|
+
attr_reader :pricing
|
|
54
|
+
|
|
55
|
+
# Provider-specific metadata as a Hash.
|
|
56
|
+
attr_reader :metadata
|
|
57
|
+
|
|
58
|
+
# The time the configured provider stopped listing the model, as a UTC
|
|
59
|
+
# Time, or +nil+ while the provider still lists it. Only a store that keeps
|
|
60
|
+
# unlisted entries, such as the Rails model table, sets it.
|
|
61
|
+
attr_reader :unlisted_at
|
|
62
|
+
|
|
63
|
+
attr_reader :reasoning_options # :nodoc:
|
|
64
|
+
|
|
65
|
+
def self.default(model_id, provider) # :nodoc:
|
|
66
|
+
new(
|
|
67
|
+
id: model_id,
|
|
68
|
+
name: model_id.tr('-', ' ').capitalize,
|
|
69
|
+
provider: provider,
|
|
70
|
+
capabilities: %w[function_calling streaming vision structured_output],
|
|
71
|
+
modalities: { input: %w[text image], output: %w[text] },
|
|
72
|
+
metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(data) # :nodoc:
|
|
77
|
+
@id = data[:id]
|
|
78
|
+
@name = data[:name]
|
|
79
|
+
@provider = data[:provider]
|
|
80
|
+
@family = data[:family]
|
|
81
|
+
@created_at = Support::Utils.to_time(data[:created_at])&.utc
|
|
82
|
+
@context_window = data[:context_window]
|
|
83
|
+
@max_output_tokens = data[:max_output_tokens]
|
|
84
|
+
@knowledge_cutoff = Support::Utils.to_date(data[:knowledge_cutoff])
|
|
85
|
+
@modalities = Modalities.new(data[:modalities] || {})
|
|
86
|
+
@capabilities = data[:capabilities] || []
|
|
87
|
+
@metadata = data[:metadata]&.dup || {}
|
|
88
|
+
@unlisted_at = Support::Utils.to_time(data[:unlisted_at])&.utc
|
|
89
|
+
@pricing = Pricing.new(pricing_data_with_long_context(data[:pricing] || {}))
|
|
90
|
+
@reasoning_options = normalize_reasoning_options(reasoning_options_from(data))
|
|
91
|
+
store_reasoning_options_metadata
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns whether #capabilities includes +capability+, given as a String
|
|
95
|
+
# or Symbol.
|
|
96
|
+
#
|
|
97
|
+
# model.supports?(:function_calling) # => true
|
|
98
|
+
#
|
|
99
|
+
def supports?(capability)
|
|
100
|
+
capabilities.include?(capability.to_s)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns whether the configured provider has stopped listing the model.
|
|
104
|
+
# An unlisted model is kept only because application records still
|
|
105
|
+
# reference it, and it may already be failing at the provider.
|
|
106
|
+
#
|
|
107
|
+
# model.unlisted? # => false
|
|
108
|
+
#
|
|
109
|
+
def unlisted?
|
|
110
|
+
!unlisted_at.nil?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the provider display name and model name combined,
|
|
114
|
+
# e.g. <tt>"OpenAI - GPT-5.6"</tt>.
|
|
115
|
+
def label
|
|
116
|
+
provider_name = provider_class&.display_name || provider
|
|
117
|
+
"#{provider_name} - #{name}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def reasoning_option(type) # :nodoc:
|
|
121
|
+
reasoning_options.find { |option| option[:type] == type.to_s }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def reasoning_option_values(type) # :nodoc:
|
|
125
|
+
Array(reasoning_option(type)&.fetch(:values, nil))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The standard text-token price columns, keyed by the symbol #price
|
|
129
|
+
# accepts.
|
|
130
|
+
PRICES = {
|
|
131
|
+
input: :input,
|
|
132
|
+
output: :output,
|
|
133
|
+
cache_read: :cache_read_input,
|
|
134
|
+
cache_write: :cache_write_input
|
|
135
|
+
}.freeze
|
|
136
|
+
private_constant :PRICES
|
|
137
|
+
|
|
138
|
+
# Returns the standard text-token price for +kind+ in USD per million
|
|
139
|
+
# tokens, or +nil+ if the registry has no such price. Valid kinds are
|
|
140
|
+
# +:input+, +:output+, +:cache_read+, and +:cache_write+.
|
|
141
|
+
#
|
|
142
|
+
# model.price(:input) # => 2.5
|
|
143
|
+
# model.price(:output) # => 10.0
|
|
144
|
+
#
|
|
145
|
+
def price(kind)
|
|
146
|
+
column = PRICES.fetch(kind) do
|
|
147
|
+
raise ArgumentError, "Unknown price kind: #{kind.inspect}. Valid kinds: #{PRICES.keys.join(', ')}"
|
|
148
|
+
end
|
|
149
|
+
pricing.text_tokens.public_send(column)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Builds a Cost for +tokens+ (a Tokens object, or anything responding
|
|
153
|
+
# to +tokens+) using this model's pricing.
|
|
154
|
+
#
|
|
155
|
+
# cost = model.cost_for(response.tokens)
|
|
156
|
+
# puts cost.total
|
|
157
|
+
#
|
|
158
|
+
# Pass <tt>tier: :batch</tt> to use the model's explicit batch rates.
|
|
159
|
+
#
|
|
160
|
+
def cost_for(tokens, tier: :standard)
|
|
161
|
+
tokens = tokens.tokens if tokens.respond_to?(:tokens)
|
|
162
|
+
|
|
163
|
+
Cost.new(tokens:, model: self, tier:)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns the Provider class registered for this model's provider slug,
|
|
167
|
+
# or +nil+ if no such provider is registered.
|
|
168
|
+
def provider_class
|
|
169
|
+
RubyLLM::Provider.resolve provider
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns the model's primary function, inferred from its output
|
|
173
|
+
# modalities: +:chat+, +:embedding+, +:moderation+, +:image+, +:audio+,
|
|
174
|
+
# +:video+, or +:rerank+.
|
|
175
|
+
def type
|
|
176
|
+
output = modalities.output
|
|
177
|
+
return :embedding if output.include?('embeddings')
|
|
178
|
+
return :moderation if output.include?('moderation')
|
|
179
|
+
return :image if output.include?('image')
|
|
180
|
+
return :audio if output.include?('audio')
|
|
181
|
+
return :video if output.include?('video')
|
|
182
|
+
return :rerank if output.include?('rerank')
|
|
183
|
+
|
|
184
|
+
:chat
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def to_h # :nodoc:
|
|
188
|
+
data = {
|
|
189
|
+
id: id,
|
|
190
|
+
name: name,
|
|
191
|
+
provider: provider,
|
|
192
|
+
family: family,
|
|
193
|
+
created_at: created_at,
|
|
194
|
+
context_window: context_window,
|
|
195
|
+
max_output_tokens: max_output_tokens,
|
|
196
|
+
knowledge_cutoff: knowledge_cutoff,
|
|
197
|
+
modalities: modalities.to_h,
|
|
198
|
+
capabilities: capabilities,
|
|
199
|
+
pricing: pricing.to_h,
|
|
200
|
+
metadata: metadata
|
|
201
|
+
}
|
|
202
|
+
# A JSON registry drops unlisted models, so the key stays out of it.
|
|
203
|
+
data[:unlisted_at] = unlisted_at if unlisted_at
|
|
204
|
+
data
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def pricing_data_with_long_context(pricing)
|
|
210
|
+
pricing = RubyLLM::Support::Utils.deep_symbolize_keys(RubyLLM::Support::Utils.deep_dup(pricing))
|
|
211
|
+
text = pricing[:text_tokens] || {}
|
|
212
|
+
return pricing if text[:long_context]
|
|
213
|
+
|
|
214
|
+
rates, threshold = PricingCategory.long_context_from_cost(metadata[:cost] || metadata['cost'])
|
|
215
|
+
return pricing unless rates
|
|
216
|
+
|
|
217
|
+
pricing[:text_tokens] = text.merge(long_context: rates)
|
|
218
|
+
pricing[:text_tokens][:long_context_threshold] = threshold if threshold
|
|
219
|
+
pricing
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def reasoning_options_from(data)
|
|
223
|
+
data[:reasoning_options] || metadata[:reasoning_options] || metadata['reasoning_options']
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def store_reasoning_options_metadata
|
|
227
|
+
return unless reasoning_options.any?
|
|
228
|
+
|
|
229
|
+
metadata.delete('reasoning_options')
|
|
230
|
+
metadata[:reasoning_options] = reasoning_options
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def normalize_reasoning_options(options)
|
|
234
|
+
Array(options).filter_map do |option|
|
|
235
|
+
next unless option.is_a?(Hash)
|
|
236
|
+
|
|
237
|
+
normalized = option.to_h.transform_keys(&:to_sym)
|
|
238
|
+
normalized[:type] = normalized[:type].to_s if normalized[:type]
|
|
239
|
+
normalized[:values] = Array(normalized[:values]).map(&:to_s) if normalized.key?(:values)
|
|
240
|
+
normalized[:default] = normalized[:default].to_s if normalized[:default].is_a?(Symbol)
|
|
241
|
+
normalized
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def inspect_attributes # :nodoc:
|
|
246
|
+
{ id: id, provider: provider, name: name }
|
|
247
|
+
end
|
|
6
248
|
end
|
|
7
249
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
class Models
|
|
7
|
+
class Aliases # :nodoc:
|
|
8
|
+
class << self
|
|
9
|
+
def resolve(model_id, provider = nil)
|
|
10
|
+
return model_id unless aliases[model_id]
|
|
11
|
+
|
|
12
|
+
if provider
|
|
13
|
+
aliases[model_id][provider.to_s] || model_id
|
|
14
|
+
else
|
|
15
|
+
aliases[model_id].values.first || model_id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def aliases
|
|
20
|
+
@aliases ||= load_aliases
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def aliases_file
|
|
24
|
+
File.expand_path('../aliases.json', __dir__)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_aliases
|
|
28
|
+
if File.exist?(aliases_file)
|
|
29
|
+
JSON.parse(File.read(aliases_file))
|
|
30
|
+
else
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reload
|
|
36
|
+
@aliases = load_aliases
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|