ruby_llm 1.15.0 → 2.0.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rdoc_options +25 -0
- data/README.md +87 -33
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -6
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +100 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -136
- data/lib/ruby_llm/active_record/attachment_helpers.rb +180 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +823 -305
- data/lib/ruby_llm/active_record/message_methods.rb +119 -75
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -150
- data/lib/ruby_llm/aliases.json +338 -167
- data/lib/ruby_llm/attachment.rb +217 -61
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1208 -150
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +274 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +141 -7
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +161 -89
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +155 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +43 -14
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +66 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +70380 -33380
- data/lib/ruby_llm/models.rb +528 -201
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +540 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +166 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +43 -34
- data/lib/ruby_llm/protocols/anthropic.rb +100 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/protocols/chat_completions/media.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +17 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +685 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +424 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +33 -20
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +565 -124
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +6 -15
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +61 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +194 -42
- data/lib/ruby_llm/providers/bedrock.rb +217 -46
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -8
- data/lib/ruby_llm/providers/deepseek/chat.rb +56 -0
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -9
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -60
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +43 -0
- data/lib/ruby_llm/providers/mistral/models.rb +57 -21
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +18 -6
- data/lib/ruby_llm/providers/ollama/chat.rb +9 -8
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -104
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +46 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +29 -21
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +164 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +10 -0
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -48
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +17 -7
- data/lib/ruby_llm/railtie.rb +11 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +139 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +385 -4
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +340 -92
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -530
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -38
- data/lib/ruby_llm/connection.rb +0 -130
- data/lib/ruby_llm/content.rb +0 -77
- data/lib/ruby_llm/mime_type.rb +0 -71
- data/lib/ruby_llm/model/info.rb +0 -130
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -257
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -57
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -69
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -403
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -90
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -322
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -543
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -96
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -232
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -221
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/media.rb +0 -84
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -53
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -70
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -203
- data/lib/ruby_llm/streaming.rb +0 -175
- data/lib/ruby_llm/utils.rb +0 -91
- data/lib/tasks/models.rake +0 -565
- data/lib/tasks/release.rake +0 -67
- data/lib/tasks/vcr.rake +0 -124
data/lib/ruby_llm/models.rb
CHANGED
|
@@ -1,172 +1,237 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
|
-
#
|
|
6
|
+
# A Models registry is the catalog of AI models RubyLLM knows about,
|
|
7
|
+
# including their capabilities, context windows, and pricing. The global
|
|
8
|
+
# registry is available through RubyLLM.models.
|
|
9
|
+
#
|
|
10
|
+
# RubyLLM.models.find 'claude-sonnet-5'
|
|
11
|
+
# RubyLLM.models.by_provider(:openai).chat_models
|
|
12
|
+
# RubyLLM.models.refresh
|
|
13
|
+
#
|
|
14
|
+
# Filter methods return new Models instances, so calls chain. Models is
|
|
15
|
+
# enumerable over its Model entries. Class-level calls such as
|
|
16
|
+
# Models.find delegate to the global registry.
|
|
5
17
|
class Models
|
|
6
18
|
include Enumerable
|
|
7
19
|
|
|
8
|
-
MODELS_DEV_PROVIDER_MAP = {
|
|
20
|
+
MODELS_DEV_PROVIDER_MAP = { # :nodoc:
|
|
9
21
|
'openai' => 'openai',
|
|
10
22
|
'anthropic' => 'anthropic',
|
|
11
23
|
'google' => 'gemini',
|
|
12
24
|
'google-vertex' => 'vertexai',
|
|
13
25
|
'amazon-bedrock' => 'bedrock',
|
|
26
|
+
'cohere' => 'cohere',
|
|
14
27
|
'deepseek' => 'deepseek',
|
|
15
28
|
'mistral' => 'mistral',
|
|
29
|
+
'ollama-cloud' => 'ollama_cloud',
|
|
16
30
|
'openrouter' => 'openrouter',
|
|
17
|
-
'perplexity' => 'perplexity'
|
|
31
|
+
'perplexity' => 'perplexity',
|
|
32
|
+
'perplexity-agent' => 'perplexity',
|
|
33
|
+
'xai' => 'xai'
|
|
18
34
|
}.freeze
|
|
35
|
+
MODELS_DEV_INPUT_MODALITIES = %w[text image audio pdf video file].freeze # :nodoc:
|
|
36
|
+
MODELS_DEV_OUTPUT_MODALITIES = %w[text image audio video embeddings moderation rerank].freeze # :nodoc:
|
|
37
|
+
# First-party providers outrank the aggregators that resell their models.
|
|
19
38
|
PROVIDER_PREFERENCE = %w[
|
|
20
39
|
openai
|
|
21
40
|
anthropic
|
|
22
41
|
gemini
|
|
23
|
-
vertexai
|
|
24
|
-
bedrock
|
|
25
|
-
openrouter
|
|
26
42
|
deepseek
|
|
27
43
|
mistral
|
|
44
|
+
cohere
|
|
28
45
|
perplexity
|
|
29
46
|
xai
|
|
47
|
+
vertexai
|
|
48
|
+
bedrock
|
|
49
|
+
openrouter
|
|
30
50
|
azure
|
|
51
|
+
ollama_cloud
|
|
31
52
|
ollama
|
|
32
53
|
gpustack
|
|
33
|
-
].freeze
|
|
54
|
+
].freeze # :nodoc:
|
|
55
|
+
INSTANCE_DELEGATES = (Enumerable.instance_methods(false) + %i[
|
|
56
|
+
all
|
|
57
|
+
each
|
|
58
|
+
find
|
|
59
|
+
listed
|
|
60
|
+
unlisted
|
|
61
|
+
chat_models
|
|
62
|
+
embedding_models
|
|
63
|
+
audio_models
|
|
64
|
+
image_models
|
|
65
|
+
by_family
|
|
66
|
+
by_provider
|
|
67
|
+
load_from_json
|
|
68
|
+
load_from_store
|
|
69
|
+
save_to_json
|
|
70
|
+
]).uniq.freeze # :nodoc:
|
|
34
71
|
|
|
35
72
|
class << self
|
|
36
|
-
|
|
73
|
+
# The providers whose model list could not be fetched during the last
|
|
74
|
+
# refresh, as hashes of +:name+, +:slug+, and +:error+. Their previous
|
|
75
|
+
# models are kept, so an unreported failure leaves stale entries behind.
|
|
76
|
+
attr_reader :last_provider_failures
|
|
77
|
+
|
|
78
|
+
INSTANCE_DELEGATES.each do |method_name|
|
|
79
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
|
80
|
+
instance.public_send(method_name, *args, **kwargs, &block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def instance # :nodoc:
|
|
37
85
|
@instance ||= new
|
|
38
86
|
end
|
|
39
87
|
|
|
40
|
-
def
|
|
41
|
-
File.expand_path('
|
|
88
|
+
def bundled_registry_file # :nodoc:
|
|
89
|
+
File.expand_path('models.json', __dir__)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def load_models # :nodoc:
|
|
93
|
+
base = models_from_store(RubyLLM.config.model_registry_store) ||
|
|
94
|
+
models_from_file(RubyLLM.config.model_registry_file) ||
|
|
95
|
+
models_from_bundle
|
|
96
|
+
|
|
97
|
+
merge_models(models_from_provider_gems, base)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def models_from_provider_gems # :nodoc:
|
|
101
|
+
Provider.model_registry_files.flat_map do |provider, file|
|
|
102
|
+
Array(models_from_file(file)).select { |model| model.provider == provider.to_s }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def models_from_store(store) # :nodoc:
|
|
107
|
+
return unless store
|
|
108
|
+
|
|
109
|
+
models = Array(store.read)
|
|
110
|
+
return models unless models.empty?
|
|
111
|
+
|
|
112
|
+
RubyLLM.logger.debug { 'Model registry store is empty, falling back to the registry file' }
|
|
113
|
+
nil
|
|
42
114
|
end
|
|
43
115
|
|
|
44
|
-
def
|
|
45
|
-
|
|
116
|
+
def models_from_file(file) # :nodoc:
|
|
117
|
+
return unless file
|
|
118
|
+
|
|
119
|
+
models = Registry.read(file)
|
|
120
|
+
models unless models.nil? || models.empty?
|
|
121
|
+
rescue ModelRegistryError => e
|
|
122
|
+
RubyLLM.logger.warn("Ignoring invalid model registry file #{file}: #{e.message}")
|
|
123
|
+
nil
|
|
46
124
|
end
|
|
47
125
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
126
|
+
def models_from_bundle # :nodoc:
|
|
127
|
+
Registry.read(bundled_registry_file) || begin
|
|
128
|
+
RubyLLM.logger.warn(
|
|
129
|
+
"Bundled model registry is missing: #{bundled_registry_file}. " \
|
|
130
|
+
'Refresh the registry to rebuild it.'
|
|
131
|
+
)
|
|
132
|
+
[]
|
|
133
|
+
end
|
|
54
134
|
end
|
|
55
135
|
|
|
56
|
-
def
|
|
57
|
-
|
|
136
|
+
def fetch_published_registry(etag: nil) # :nodoc:
|
|
137
|
+
Registry::PublishedSource.new.fetch(etag:)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Refreshes the global model registry from the published catalog and
|
|
141
|
+
# configured providers. Returns the global Models instance. See
|
|
142
|
+
# #refresh for the +remote_only:+ option.
|
|
143
|
+
def refresh(remote_only: false)
|
|
144
|
+
instance.refresh(remote_only: remote_only)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def refresh_from_providers(remote_only: false) # :nodoc:
|
|
148
|
+
instance.refresh_from_providers(remote_only: remote_only)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# :stopdoc:
|
|
58
152
|
|
|
59
|
-
|
|
60
|
-
|
|
153
|
+
# Fetches and merges models directly from upstream provider APIs and
|
|
154
|
+
# models.dev for the maintainer registry builder.
|
|
155
|
+
def fetch_merged_models(remote_only: false) # :nodoc:
|
|
156
|
+
RubyLLM.instrument('models.refresh.ruby_llm', remote_only:) do |payload|
|
|
157
|
+
existing_models = read_existing_models
|
|
61
158
|
|
|
62
|
-
|
|
63
|
-
|
|
159
|
+
provider_fetch = fetch_provider_models(remote_only: remote_only)
|
|
160
|
+
@last_provider_failures = provider_fetch[:failed]
|
|
161
|
+
log_provider_fetch(provider_fetch)
|
|
162
|
+
payload[:failed_providers] = provider_fetch[:failed].map { |failure| failure[:slug] }
|
|
64
163
|
|
|
65
|
-
|
|
66
|
-
|
|
164
|
+
models_dev_fetch = fetch_models_dev_models(existing_models)
|
|
165
|
+
log_models_dev_fetch(models_dev_fetch)
|
|
166
|
+
|
|
167
|
+
merged_models = merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
|
|
168
|
+
payload[:model_count] = merged_models.size
|
|
169
|
+
merged_models
|
|
170
|
+
end
|
|
67
171
|
end
|
|
68
172
|
|
|
69
|
-
def fetch_provider_models(remote_only: true) #
|
|
173
|
+
def fetch_provider_models(remote_only: true) # :nodoc:
|
|
70
174
|
config = RubyLLM.config
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Provider.configured_remote_providers(config)
|
|
74
|
-
else
|
|
75
|
-
Provider.configured_providers(config)
|
|
76
|
-
end
|
|
77
|
-
configured = configured_classes.select { |klass| provider_classes.include?(klass) }
|
|
175
|
+
providers = remote_only ? Provider.configured_remote_providers(config) : Provider.configured_providers(config)
|
|
176
|
+
providers = providers.reject { |provider| Provider.model_registry_files.key?(provider.slug.to_sym) }
|
|
78
177
|
result = {
|
|
79
|
-
models: [],
|
|
80
|
-
fetched_providers: [],
|
|
81
|
-
configured_names: configured.map(&:name),
|
|
82
|
-
failed: []
|
|
178
|
+
models: [], fetched_providers: [], configured_names: providers.map(&:display_name), failed: [], empty: []
|
|
83
179
|
}
|
|
84
180
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
result[:models].concat(
|
|
181
|
+
providers.each do |provider_class|
|
|
182
|
+
models = provider_class.new(config).list_models
|
|
183
|
+
if models.empty?
|
|
184
|
+
result[:empty] << { name: provider_class.display_name, slug: provider_class.slug }
|
|
185
|
+
else
|
|
186
|
+
result[:models].concat(models)
|
|
91
187
|
result[:fetched_providers] << provider_class.slug
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
result[:failed] << { name: provider_class.name, slug: provider_class.slug, error: e }
|
|
94
188
|
end
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
result[:failed] << { name: provider_class.display_name, slug: provider_class.slug, error: e }
|
|
95
191
|
end
|
|
96
192
|
|
|
97
|
-
result[:fetched_providers].uniq!
|
|
98
193
|
result
|
|
99
194
|
end
|
|
100
195
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
fetch_provider_models(remote_only: remote_only)[:models]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def resolve(model_id, provider: nil, assume_exists: false, config: nil) # rubocop:disable Metrics/PerceivedComplexity
|
|
196
|
+
def resolve(model_id, provider: nil, assume_model_exists: false, config: nil,
|
|
197
|
+
operation: nil, default_model: nil) # rubocop:disable Metrics/PerceivedComplexity
|
|
107
198
|
config ||= RubyLLM.config
|
|
108
199
|
provider_class = provider ? Provider.providers[provider.to_sym] : nil
|
|
200
|
+
if operation && provider_class && !provider_class.model_required?(operation:)
|
|
201
|
+
raise ArgumentError, "#{operation} does not accept a model" unless model_id.nil?
|
|
109
202
|
|
|
110
|
-
|
|
111
|
-
temp_instance = provider_class.new(config)
|
|
112
|
-
assume_exists = true if temp_instance.local? || temp_instance.assume_models_exist?
|
|
203
|
+
return [nil, provider_class.new(config)]
|
|
113
204
|
end
|
|
205
|
+
model_id ||= default_model
|
|
206
|
+
assume_model_exists = true if provider_class&.local? || provider_class&.assume_models_exist?
|
|
114
207
|
|
|
115
|
-
if
|
|
116
|
-
raise ArgumentError, 'Provider must be specified if
|
|
117
|
-
|
|
118
|
-
provider_class ||= raise(Error, "Unknown provider: #{provider.to_sym}")
|
|
119
|
-
provider_instance = provider_class.new(config)
|
|
208
|
+
if assume_model_exists
|
|
209
|
+
raise ArgumentError, 'Provider must be specified if assume_model_exists is true' unless provider
|
|
120
210
|
|
|
121
|
-
|
|
122
|
-
begin
|
|
123
|
-
Models.find(model_id, provider)
|
|
124
|
-
rescue ModelNotFoundError
|
|
125
|
-
nil
|
|
126
|
-
end
|
|
127
|
-
end
|
|
211
|
+
provider_class ||= Provider.resolve!(provider)
|
|
128
212
|
|
|
129
|
-
model
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
provider_instance = provider_class.new(config)
|
|
135
|
-
end
|
|
136
|
-
[model, provider_instance]
|
|
137
|
-
end
|
|
213
|
+
model = begin
|
|
214
|
+
Models.find(model_id, provider: provider, config: config)
|
|
215
|
+
rescue ModelNotFoundError
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
138
218
|
|
|
139
|
-
|
|
140
|
-
if instance.respond_to?(method)
|
|
141
|
-
instance.send(method, ...)
|
|
219
|
+
model ||= Model.default(model_id, provider_class.slug)
|
|
142
220
|
else
|
|
143
|
-
|
|
221
|
+
model = Models.find model_id, provider: provider, config: config
|
|
222
|
+
provider_class = Provider.resolve!(model.provider)
|
|
144
223
|
end
|
|
224
|
+
[model, provider_class.new(config)]
|
|
145
225
|
end
|
|
146
226
|
|
|
147
|
-
def
|
|
148
|
-
instance.respond_to?(method, include_private) || super
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def fetch_models_dev_models(existing_models) # rubocop:disable Metrics/PerceivedComplexity
|
|
227
|
+
def fetch_models_dev_models(existing_models) # :nodoc:
|
|
152
228
|
RubyLLM.logger.info 'Fetching models from models.dev API...'
|
|
153
229
|
|
|
154
|
-
connection = Connection.basic do |f|
|
|
230
|
+
connection = Transport::Connection.basic do |f|
|
|
155
231
|
f.request :json
|
|
156
232
|
f.response :json, parser_options: { symbolize_names: true }
|
|
157
233
|
end
|
|
158
|
-
|
|
159
|
-
providers = response.body || {}
|
|
160
|
-
|
|
161
|
-
models = providers.flat_map do |provider_key, provider_data|
|
|
162
|
-
provider_slug = MODELS_DEV_PROVIDER_MAP[provider_key.to_s]
|
|
163
|
-
next [] unless provider_slug
|
|
164
|
-
|
|
165
|
-
(provider_data[:models] || {}).values.map do |model_data|
|
|
166
|
-
Model::Info.new(models_dev_model_to_info(model_data, provider_slug, provider_key.to_s))
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
{ models: models.reject { |model| model.provider.nil? || model.id.nil? }, fetched: true }
|
|
234
|
+
{ models: parse_models_dev_catalog(connection.get('https://models.dev/api.json').body), fetched: true }
|
|
170
235
|
rescue StandardError => e
|
|
171
236
|
RubyLLM.logger.warn("Failed to fetch models.dev (#{e.class}: #{e.message}). Keeping existing.")
|
|
172
237
|
{
|
|
@@ -175,13 +240,33 @@ module RubyLLM
|
|
|
175
240
|
}
|
|
176
241
|
end
|
|
177
242
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
243
|
+
# An answer RubyLLM cannot read a single model out of is no answer:
|
|
244
|
+
# only a catalog carrying models may overrule what the registry holds.
|
|
245
|
+
def parse_models_dev_catalog(body) # :nodoc:
|
|
246
|
+
raise ModelRegistryError, "models.dev returned #{body.class} instead of a catalog" unless body.is_a?(Hash)
|
|
247
|
+
|
|
248
|
+
models = body.flat_map { |provider_key, data| models_dev_provider_models(provider_key, data) }
|
|
249
|
+
raise ModelRegistryError, 'models.dev returned no models RubyLLM knows a provider for' if models.empty?
|
|
250
|
+
|
|
251
|
+
models
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def models_dev_provider_models(provider_key, provider_data) # :nodoc:
|
|
255
|
+
provider_slug = MODELS_DEV_PROVIDER_MAP[provider_key.to_s]
|
|
256
|
+
return [] unless provider_slug
|
|
257
|
+
|
|
258
|
+
(provider_data[:models] || {}).values.filter_map do |model_data|
|
|
259
|
+
model = Model.new(models_dev_model_attributes(model_data, provider_slug, provider_key.to_s))
|
|
260
|
+
model unless model.provider.nil? || model.id.nil?
|
|
261
|
+
end
|
|
182
262
|
end
|
|
183
263
|
|
|
184
|
-
def
|
|
264
|
+
def read_existing_models # :nodoc:
|
|
265
|
+
existing_models = instance.all
|
|
266
|
+
existing_models.empty? ? load_models : existing_models
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def log_provider_fetch(provider_fetch) # :nodoc:
|
|
185
270
|
RubyLLM.logger.info "Fetching models from providers: #{provider_fetch[:configured_names].join(', ')}"
|
|
186
271
|
provider_fetch[:failed].each do |failure|
|
|
187
272
|
RubyLLM.logger.warn(
|
|
@@ -189,15 +274,18 @@ module RubyLLM
|
|
|
189
274
|
'Keeping existing.'
|
|
190
275
|
)
|
|
191
276
|
end
|
|
277
|
+
Array(provider_fetch[:empty]).each do |provider|
|
|
278
|
+
RubyLLM.logger.warn("#{provider[:name]} listed no models. Keeping existing.")
|
|
279
|
+
end
|
|
192
280
|
end
|
|
193
281
|
|
|
194
|
-
def log_models_dev_fetch(models_dev_fetch)
|
|
282
|
+
def log_models_dev_fetch(models_dev_fetch) # :nodoc:
|
|
195
283
|
return if models_dev_fetch[:fetched]
|
|
196
284
|
|
|
197
285
|
RubyLLM.logger.warn('Using cached models.dev data due to fetch failure.')
|
|
198
286
|
end
|
|
199
287
|
|
|
200
|
-
def merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
|
|
288
|
+
def merge_with_existing(existing_models, provider_fetch, models_dev_fetch) # :nodoc:
|
|
201
289
|
existing_by_provider = existing_models.group_by(&:provider)
|
|
202
290
|
preserved_models = existing_by_provider
|
|
203
291
|
.except(*provider_fetch[:fetched_providers])
|
|
@@ -214,70 +302,50 @@ module RubyLLM
|
|
|
214
302
|
merge_models(provider_models, models_dev_models)
|
|
215
303
|
end
|
|
216
304
|
|
|
217
|
-
def merge_models(provider_models, models_dev_models)
|
|
305
|
+
def merge_models(provider_models, models_dev_models) # :nodoc:
|
|
218
306
|
models_dev_by_key = index_by_key(models_dev_models)
|
|
219
307
|
provider_by_key = index_by_key(provider_models)
|
|
308
|
+
provider_by_alias = index_provider_aliases(provider_models)
|
|
220
309
|
|
|
221
310
|
all_keys = models_dev_by_key.keys | provider_by_key.keys
|
|
222
311
|
|
|
223
312
|
models = all_keys.map do |key|
|
|
224
|
-
|
|
225
|
-
|
|
313
|
+
provider_model = provider_by_key[key] || provider_by_alias[key]
|
|
314
|
+
models_dev_model = find_models_dev_model(key, models_dev_by_key, provider_model)
|
|
226
315
|
|
|
227
316
|
if models_dev_model && provider_model
|
|
228
317
|
add_provider_metadata(models_dev_model, provider_model)
|
|
229
318
|
elsif models_dev_model
|
|
230
319
|
models_dev_model
|
|
231
320
|
else
|
|
232
|
-
provider_model
|
|
321
|
+
augment_model_capabilities(provider_model)
|
|
233
322
|
end
|
|
234
323
|
end
|
|
235
324
|
|
|
236
|
-
|
|
325
|
+
models.sort_by { |m| [m.provider, m.id] }
|
|
237
326
|
end
|
|
238
327
|
|
|
239
|
-
def
|
|
240
|
-
models.reject do |model|
|
|
241
|
-
model.provider.to_s == 'vertexai' && model.id.to_s.include?('/')
|
|
242
|
-
end
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def find_models_dev_model(key, models_dev_by_key)
|
|
246
|
-
# Direct match
|
|
328
|
+
def find_models_dev_model(key, models_dev_by_key, provider_model = nil) # :nodoc:
|
|
247
329
|
return models_dev_by_key[key] if models_dev_by_key[key]
|
|
248
330
|
|
|
249
331
|
provider, model_id = key.split(':', 2)
|
|
250
|
-
|
|
251
|
-
normalized_id = model_id.sub(/^[a-z]{2}\./, '')
|
|
252
|
-
context_override = nil
|
|
253
|
-
normalized_id = normalized_id.gsub(/:(\d+)k\b/) do
|
|
254
|
-
context_override = Regexp.last_match(1).to_i * 1000
|
|
255
|
-
''
|
|
256
|
-
end
|
|
257
|
-
bedrock_model = models_dev_by_key["bedrock:#{normalized_id}"]
|
|
258
|
-
if bedrock_model
|
|
259
|
-
data = bedrock_model.to_h.merge(id: model_id)
|
|
260
|
-
data[:context_window] = context_override if context_override
|
|
261
|
-
return Model::Info.new(data)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# VertexAI uses same models as Gemini
|
|
266
|
-
return unless provider == 'vertexai'
|
|
267
|
-
|
|
268
|
-
gemini_model = models_dev_by_key["gemini:#{model_id}"]
|
|
269
|
-
return unless gemini_model
|
|
270
|
-
|
|
271
|
-
# Return Gemini's models.dev data but with VertexAI as provider
|
|
272
|
-
Model::Info.new(gemini_model.to_h.merge(provider: 'vertexai'))
|
|
332
|
+
Provider.resolve(provider)&.models_dev_alias(model_id, models_dev_by_key, provider_model)
|
|
273
333
|
end
|
|
274
334
|
|
|
275
|
-
def index_by_key(models)
|
|
335
|
+
def index_by_key(models) # :nodoc:
|
|
276
336
|
models.to_h do |model|
|
|
277
337
|
["#{model.provider}:#{model.id}", model]
|
|
278
338
|
end
|
|
279
339
|
end
|
|
280
340
|
|
|
341
|
+
def index_provider_aliases(models) # :nodoc:
|
|
342
|
+
models.each_with_object({}) do |model, aliases|
|
|
343
|
+
Array(model.metadata[:aliases]).each do |alias_id|
|
|
344
|
+
aliases["#{model.provider}:#{alias_id}"] ||= model
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
281
349
|
def add_provider_metadata(models_dev_model, provider_model) # rubocop:disable Metrics/PerceivedComplexity
|
|
282
350
|
data = models_dev_model.to_h
|
|
283
351
|
data[:name] = provider_model.name if blank_value?(data[:name])
|
|
@@ -285,15 +353,51 @@ module RubyLLM
|
|
|
285
353
|
data[:created_at] = provider_model.created_at if blank_value?(data[:created_at])
|
|
286
354
|
data[:context_window] = provider_model.context_window if blank_value?(data[:context_window])
|
|
287
355
|
data[:max_output_tokens] = provider_model.max_output_tokens if blank_value?(data[:max_output_tokens])
|
|
356
|
+
data[:knowledge_cutoff] = provider_model.knowledge_cutoff if blank_value?(data[:knowledge_cutoff])
|
|
288
357
|
data[:modalities] = provider_model.modalities.to_h if blank_value?(data[:modalities])
|
|
289
|
-
|
|
358
|
+
if models_dev_model.type == :chat && provider_model.type != :chat
|
|
359
|
+
data[:modalities] = provider_model.modalities.to_h
|
|
360
|
+
end
|
|
361
|
+
data[:pricing] = Support::Utils.deep_merge(provider_model.pricing.to_h, data[:pricing].to_h)
|
|
290
362
|
data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
|
|
291
|
-
data[:capabilities] = (models_dev_model
|
|
363
|
+
data[:capabilities] = merge_capabilities(models_dev_model, provider_model, data[:modalities])
|
|
292
364
|
normalize_embedding_modalities(data)
|
|
293
|
-
Model
|
|
365
|
+
Model.new(data)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def merge_capabilities(models_dev_model, provider_model, modalities) # :nodoc:
|
|
369
|
+
denied = models_dev_reported_capabilities(models_dev_model) - models_dev_model.capabilities
|
|
370
|
+
reported = (models_dev_model.capabilities + provider_model.capabilities).uniq - denied
|
|
371
|
+
augment_capabilities(provider_model.provider, reported, provider_model.id, modalities)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def augment_model_capabilities(model) # :nodoc:
|
|
375
|
+
capabilities = augment_capabilities(model.provider, model.capabilities, model.id, model.modalities.to_h)
|
|
376
|
+
return model if capabilities == model.capabilities
|
|
377
|
+
|
|
378
|
+
Model.new(model.to_h.merge(capabilities: capabilities))
|
|
294
379
|
end
|
|
295
380
|
|
|
296
|
-
def
|
|
381
|
+
def augment_capabilities(provider_slug, capabilities, model_id, modalities) # :nodoc:
|
|
382
|
+
augmenter = Provider.resolve(provider_slug)&.capabilities
|
|
383
|
+
return capabilities unless augmenter
|
|
384
|
+
|
|
385
|
+
augmenter.augment(capabilities, model_id: model_id, modalities: modalities.to_h)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# models.dev leaves a field out where it has no opinion, so only the
|
|
389
|
+
# capabilities it reports on can overrule what a provider claims.
|
|
390
|
+
def models_dev_reported_capabilities(models_dev_model) # :nodoc:
|
|
391
|
+
metadata = models_dev_model.metadata
|
|
392
|
+
reported = []
|
|
393
|
+
reported << 'function_calling' unless metadata[:tool_call].nil?
|
|
394
|
+
reported << 'structured_output' unless metadata[:structured_output].nil?
|
|
395
|
+
reported << 'reasoning' unless metadata[:reasoning].nil? && metadata[:reasoning_options].nil?
|
|
396
|
+
reported << 'vision' unless models_dev_model.modalities.input.empty?
|
|
397
|
+
reported
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def normalize_embedding_modalities(data) # :nodoc:
|
|
297
401
|
return unless data[:id].to_s.include?('embedding')
|
|
298
402
|
|
|
299
403
|
modalities = data[:modalities].to_h
|
|
@@ -302,7 +406,7 @@ module RubyLLM
|
|
|
302
406
|
data[:modalities] = modalities
|
|
303
407
|
end
|
|
304
408
|
|
|
305
|
-
def blank_value?(value)
|
|
409
|
+
def blank_value?(value) # :nodoc:
|
|
306
410
|
return true if value.nil?
|
|
307
411
|
return value.empty? if value.is_a?(String) || value.is_a?(Array)
|
|
308
412
|
|
|
@@ -315,19 +419,19 @@ module RubyLLM
|
|
|
315
419
|
false
|
|
316
420
|
end
|
|
317
421
|
|
|
318
|
-
def
|
|
422
|
+
def models_dev_model_attributes(model_data, provider_slug, provider_key) # :nodoc:
|
|
319
423
|
modalities = normalize_models_dev_modalities(model_data[:modalities])
|
|
320
|
-
capabilities = models_dev_capabilities(model_data, modalities)
|
|
424
|
+
capabilities = models_dev_capabilities(model_data, modalities, provider_slug)
|
|
321
425
|
|
|
322
426
|
created_date = [model_data[:release_date], model_data[:last_updated]]
|
|
323
427
|
.find { |value| !value.to_s.strip.empty? }
|
|
324
428
|
|
|
325
429
|
data = {
|
|
326
|
-
id: model_data[:id],
|
|
430
|
+
id: models_dev_model_id(model_data[:id], provider_slug),
|
|
327
431
|
name: model_data[:name] || model_data[:id],
|
|
328
432
|
provider: provider_slug,
|
|
329
433
|
family: model_data[:family],
|
|
330
|
-
created_at: created_date
|
|
434
|
+
created_at: Support::Utils.iso_date_prefix_to_utc_midnight_string(created_date),
|
|
331
435
|
context_window: model_data.dig(:limit, :context),
|
|
332
436
|
max_output_tokens: model_data.dig(:limit, :output),
|
|
333
437
|
knowledge_cutoff: normalize_models_dev_knowledge(model_data[:knowledge]),
|
|
@@ -341,16 +445,22 @@ module RubyLLM
|
|
|
341
445
|
data
|
|
342
446
|
end
|
|
343
447
|
|
|
344
|
-
def
|
|
448
|
+
def models_dev_model_id(id, provider_slug) # :nodoc:
|
|
449
|
+
provider = Provider.resolve(provider_slug)
|
|
450
|
+
provider ? provider.models_dev_model_id(id) : id
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def models_dev_capabilities(model_data, modalities, provider_slug) # :nodoc:
|
|
345
454
|
capabilities = []
|
|
346
455
|
capabilities << 'function_calling' if model_data[:tool_call]
|
|
347
456
|
capabilities << 'structured_output' if model_data[:structured_output]
|
|
348
|
-
capabilities << 'reasoning' if model_data[:reasoning]
|
|
457
|
+
capabilities << 'reasoning' if model_data[:reasoning] || model_data[:reasoning_options]
|
|
349
458
|
capabilities << 'vision' if modalities[:input].intersect?(%w[image video pdf])
|
|
350
|
-
capabilities.
|
|
459
|
+
capabilities << 'video' if modalities[:input].include?('video')
|
|
460
|
+
augment_capabilities(provider_slug, capabilities.uniq, model_data[:id], modalities)
|
|
351
461
|
end
|
|
352
462
|
|
|
353
|
-
def models_dev_pricing(cost)
|
|
463
|
+
def models_dev_pricing(cost) # :nodoc:
|
|
354
464
|
return {} unless cost
|
|
355
465
|
|
|
356
466
|
text_standard = {
|
|
@@ -367,12 +477,27 @@ module RubyLLM
|
|
|
367
477
|
}.compact
|
|
368
478
|
|
|
369
479
|
pricing = {}
|
|
370
|
-
|
|
480
|
+
text_tokens = models_dev_text_tokens_pricing(text_standard, cost)
|
|
481
|
+
pricing[:text_tokens] = text_tokens if text_tokens
|
|
371
482
|
pricing[:audio_tokens] = { standard: audio_standard } if audio_standard.any?
|
|
372
483
|
pricing
|
|
373
484
|
end
|
|
374
485
|
|
|
375
|
-
def
|
|
486
|
+
def models_dev_text_tokens_pricing(text_standard, cost) # :nodoc:
|
|
487
|
+
long_context, threshold = Model::PricingCategory.long_context_from_cost(cost)
|
|
488
|
+
|
|
489
|
+
return nil if text_standard.empty? && long_context.nil?
|
|
490
|
+
|
|
491
|
+
text_tokens = {}
|
|
492
|
+
text_tokens[:standard] = text_standard if text_standard.any?
|
|
493
|
+
if long_context
|
|
494
|
+
text_tokens[:long_context] = long_context
|
|
495
|
+
text_tokens[:long_context_threshold] = threshold if threshold
|
|
496
|
+
end
|
|
497
|
+
text_tokens
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def models_dev_metadata(model_data, provider_key) # :nodoc:
|
|
376
501
|
metadata = {
|
|
377
502
|
source: 'models.dev',
|
|
378
503
|
provider_id: provider_key,
|
|
@@ -382,6 +507,10 @@ module RubyLLM
|
|
|
382
507
|
last_updated: model_data[:last_updated],
|
|
383
508
|
status: model_data[:status],
|
|
384
509
|
interleaved: model_data[:interleaved],
|
|
510
|
+
tool_call: model_data[:tool_call],
|
|
511
|
+
structured_output: model_data[:structured_output],
|
|
512
|
+
reasoning: model_data[:reasoning],
|
|
513
|
+
reasoning_options: model_data[:reasoning_options],
|
|
385
514
|
cost: model_data[:cost],
|
|
386
515
|
limit: model_data[:limit],
|
|
387
516
|
knowledge: model_data[:knowledge]
|
|
@@ -389,16 +518,16 @@ module RubyLLM
|
|
|
389
518
|
metadata.compact
|
|
390
519
|
end
|
|
391
520
|
|
|
392
|
-
def normalize_models_dev_modalities(modalities)
|
|
521
|
+
def normalize_models_dev_modalities(modalities) # :nodoc:
|
|
393
522
|
normalized = { input: [], output: [] }
|
|
394
523
|
return normalized unless modalities
|
|
395
524
|
|
|
396
|
-
normalized[:input] = Array(modalities[:input]).compact
|
|
397
|
-
normalized[:output] = Array(modalities[:output]).compact
|
|
525
|
+
normalized[:input] = Array(modalities[:input]).compact & MODELS_DEV_INPUT_MODALITIES
|
|
526
|
+
normalized[:output] = Array(modalities[:output]).compact & MODELS_DEV_OUTPUT_MODALITIES
|
|
398
527
|
normalized
|
|
399
528
|
end
|
|
400
529
|
|
|
401
|
-
def normalize_models_dev_knowledge(value)
|
|
530
|
+
def normalize_models_dev_knowledge(value) # :nodoc:
|
|
402
531
|
return if value.nil?
|
|
403
532
|
return value if value.is_a?(Date)
|
|
404
533
|
|
|
@@ -408,99 +537,297 @@ module RubyLLM
|
|
|
408
537
|
end
|
|
409
538
|
end
|
|
410
539
|
|
|
411
|
-
|
|
412
|
-
|
|
540
|
+
# :startdoc:
|
|
541
|
+
|
|
542
|
+
def initialize(models = nil) # :nodoc:
|
|
543
|
+
@models = models || self.class.load_models
|
|
413
544
|
end
|
|
414
545
|
|
|
415
|
-
|
|
416
|
-
|
|
546
|
+
# Replaces the models in this registry with those read from the JSON
|
|
547
|
+
# +file+. The default is the configured
|
|
548
|
+
# <tt>RubyLLM.config.model_registry_file</tt>. A missing or invalid
|
|
549
|
+
# file falls back to the registry bundled with the gem.
|
|
550
|
+
def load_from_json(file = RubyLLM.config.model_registry_file)
|
|
551
|
+
@models = self.class.models_from_file(file) || self.class.models_from_bundle
|
|
552
|
+
self
|
|
417
553
|
end
|
|
418
554
|
|
|
555
|
+
# Replaces the models in this registry with entries from the configured
|
|
556
|
+
# model-registry store.
|
|
557
|
+
def load_from_store
|
|
558
|
+
store = RubyLLM.config.model_registry_store
|
|
559
|
+
raise ModelRegistryError, 'No model registry store is configured' unless store
|
|
560
|
+
|
|
561
|
+
@models = Array(store.read)
|
|
562
|
+
self
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Exports this registry to +file+ as pretty-printed JSON. The default is
|
|
566
|
+
# the configured <tt>RubyLLM.config.model_registry_file</tt>. A regular
|
|
567
|
+
# #refresh already persists to the active registry store.
|
|
568
|
+
#
|
|
569
|
+
# RubyLLM.models.save_to_json('/tmp/models.json')
|
|
570
|
+
#
|
|
419
571
|
def save_to_json(file = RubyLLM.config.model_registry_file)
|
|
420
|
-
|
|
572
|
+
Registry::FileStore.new(file).write(all)
|
|
573
|
+
self
|
|
421
574
|
end
|
|
422
575
|
|
|
576
|
+
# Returns an array of the Model entries the configured provider still
|
|
577
|
+
# lists. Models it has stopped listing are left out; #find still resolves
|
|
578
|
+
# them, and #unlisted reports them.
|
|
423
579
|
def all
|
|
580
|
+
all_including_unlisted.reject(&:unlisted?)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Returns an array of the Model entries the configured provider has
|
|
584
|
+
# stopped listing. Only a store that keeps them, such as the Rails model
|
|
585
|
+
# table, ever reports one.
|
|
586
|
+
#
|
|
587
|
+
# RubyLLM.models.unlisted.map(&:id)
|
|
588
|
+
#
|
|
589
|
+
def unlisted
|
|
590
|
+
all_including_unlisted.select(&:unlisted?)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# Returns an array of the Model entries the configured provider still
|
|
594
|
+
# lists. Reads the same as #all, which already excludes the rest.
|
|
595
|
+
alias listed all
|
|
596
|
+
|
|
597
|
+
def all_including_unlisted # :nodoc:
|
|
424
598
|
@models
|
|
425
599
|
end
|
|
426
600
|
|
|
601
|
+
# Yields each Model in the registry.
|
|
602
|
+
#
|
|
603
|
+
# RubyLLM.models.each { |model| puts model.id }
|
|
604
|
+
#
|
|
427
605
|
def each(&)
|
|
428
606
|
all.each(&)
|
|
429
607
|
end
|
|
430
608
|
|
|
431
|
-
|
|
609
|
+
# Returns the Model matching +model_id+, resolving aliases along the
|
|
610
|
+
# way. Without +provider+, picks the preferred provider that carries
|
|
611
|
+
# the model, first-party providers before aggregators. Raises
|
|
612
|
+
# RubyLLM::ModelNotFoundError if no model matches.
|
|
613
|
+
#
|
|
614
|
+
# RubyLLM.models.find 'gpt-5.6'
|
|
615
|
+
# RubyLLM.models.find 'claude-sonnet-5', provider: :bedrock
|
|
616
|
+
#
|
|
617
|
+
def find(model_id, provider: nil, config: nil)
|
|
432
618
|
if provider
|
|
433
|
-
find_with_provider(model_id, provider)
|
|
619
|
+
find_with_provider(model_id, provider, config)
|
|
434
620
|
else
|
|
435
621
|
find_without_provider(model_id)
|
|
436
622
|
end
|
|
437
623
|
end
|
|
438
624
|
|
|
625
|
+
# Returns a new Models registry containing only chat models.
|
|
439
626
|
def chat_models
|
|
440
|
-
|
|
627
|
+
select_models { |m| m.type == :chat }
|
|
441
628
|
end
|
|
442
629
|
|
|
630
|
+
# Returns a new Models registry containing only embedding models.
|
|
443
631
|
def embedding_models
|
|
444
|
-
|
|
632
|
+
select_models { |m| m.type == :embedding || m.modalities.output.include?('embeddings') }
|
|
445
633
|
end
|
|
446
634
|
|
|
635
|
+
# Returns a new Models registry containing only models with audio
|
|
636
|
+
# output.
|
|
447
637
|
def audio_models
|
|
448
|
-
|
|
638
|
+
select_models { |m| m.type == :audio || m.modalities.output.include?('audio') }
|
|
449
639
|
end
|
|
450
640
|
|
|
641
|
+
# Returns a new Models registry containing only models with image
|
|
642
|
+
# output.
|
|
451
643
|
def image_models
|
|
452
|
-
|
|
644
|
+
select_models { |m| m.type == :image || m.modalities.output.include?('image') }
|
|
453
645
|
end
|
|
454
646
|
|
|
647
|
+
# Returns a new Models registry containing only models in +family+.
|
|
648
|
+
#
|
|
649
|
+
# RubyLLM.models.by_family('claude3_sonnet')
|
|
650
|
+
#
|
|
455
651
|
def by_family(family)
|
|
456
|
-
|
|
652
|
+
select_models { |m| m.family == family.to_s }
|
|
457
653
|
end
|
|
458
654
|
|
|
655
|
+
# Returns a new Models registry containing only models from +provider+.
|
|
656
|
+
# Accepts a symbol or a string.
|
|
657
|
+
#
|
|
658
|
+
# RubyLLM.models.by_provider(:openai).select { |model| model.supports?(:vision) }
|
|
659
|
+
#
|
|
459
660
|
def by_provider(provider)
|
|
460
|
-
|
|
661
|
+
select_models { |m| m.provider == provider.to_s }
|
|
461
662
|
end
|
|
462
663
|
|
|
463
|
-
|
|
464
|
-
|
|
664
|
+
# Replaces the registry with the latest published RubyLLM catalog,
|
|
665
|
+
# merged with models discovered from configured providers. The result is
|
|
666
|
+
# saved to the platform cache, or to the database in Rails applications.
|
|
667
|
+
# Pass +remote_only:+ +true+ to skip local providers such as Ollama and
|
|
668
|
+
# GPUStack. Returns +self+.
|
|
669
|
+
#
|
|
670
|
+
# Raises ModelRegistryError when the catalog cannot be fetched or the
|
|
671
|
+
# result cannot be persisted, leaving the current registry unchanged.
|
|
672
|
+
#
|
|
673
|
+
# RubyLLM.models.refresh
|
|
674
|
+
# RubyLLM.models.refresh(remote_only: true).chat_models
|
|
675
|
+
#
|
|
676
|
+
def refresh(remote_only: false)
|
|
677
|
+
RubyLLM.instrument('models.refresh.ruby_llm', remote_only:) do |payload|
|
|
678
|
+
published = fetch_published_models
|
|
679
|
+
main_models = merge_discovered_models(published.models, remote_only:)
|
|
680
|
+
merged_models = self.class.merge_models(self.class.models_from_provider_gems, main_models)
|
|
681
|
+
persisted_models = RubyLLM.config.model_registry_store ? merged_models : main_models
|
|
682
|
+
persist_registry!(persisted_models, published:)
|
|
683
|
+
@models = stored_models || merged_models
|
|
684
|
+
payload.merge!(model_count: all.size, not_modified: published.not_modified)
|
|
685
|
+
end
|
|
686
|
+
self
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def refresh_from_providers(remote_only: false) # :nodoc:
|
|
690
|
+
@models = self.class.fetch_merged_models(remote_only: remote_only)
|
|
691
|
+
self
|
|
465
692
|
end
|
|
466
693
|
|
|
467
|
-
def resolve(model_id, provider: nil,
|
|
468
|
-
self.class.resolve(model_id, provider: provider,
|
|
694
|
+
def resolve(model_id, provider: nil, assume_model_exists: false, config: nil) # :nodoc:
|
|
695
|
+
self.class.resolve(model_id, provider: provider, assume_model_exists: assume_model_exists, config: config)
|
|
469
696
|
end
|
|
470
697
|
|
|
471
698
|
private
|
|
472
699
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
all.find { |m| m.id == model_id && m.provider == provider.to_s } ||
|
|
478
|
-
raise(ModelNotFoundError, "Unknown model: #{model_id} for provider: #{provider}")
|
|
700
|
+
# Filters keep the unlisted entries so #find and #unlisted still see them
|
|
701
|
+
# after a chain such as by_provider(:openai).unlisted.
|
|
702
|
+
def select_models(&)
|
|
703
|
+
self.class.new(all_including_unlisted.select(&))
|
|
479
704
|
end
|
|
480
705
|
|
|
481
|
-
def
|
|
482
|
-
|
|
483
|
-
return
|
|
706
|
+
def file_store
|
|
707
|
+
return if RubyLLM.config.model_registry_store
|
|
708
|
+
return unless RubyLLM.config.model_registry_file
|
|
484
709
|
|
|
485
|
-
|
|
486
|
-
|
|
710
|
+
Registry::FileStore.new(RubyLLM.config.model_registry_file)
|
|
711
|
+
end
|
|
487
712
|
|
|
488
|
-
|
|
489
|
-
|
|
713
|
+
# The ETag identifies the published catalog, not the merged registry, so
|
|
714
|
+
# it may only be sent while the catalog it stands for is still on disk.
|
|
715
|
+
def published_store
|
|
716
|
+
file = file_store
|
|
717
|
+
Registry::FileStore.new("#{file.path}.published.json") if file
|
|
718
|
+
end
|
|
490
719
|
|
|
491
|
-
|
|
492
|
-
|
|
720
|
+
def fetch_published_models
|
|
721
|
+
cached = published_catalog
|
|
722
|
+
result = self.class.fetch_published_registry(etag: (file_store.etag if cached))
|
|
723
|
+
result.models ||= cached
|
|
724
|
+
result.models ? result : self.class.fetch_published_registry
|
|
493
725
|
end
|
|
494
726
|
|
|
495
|
-
def
|
|
496
|
-
|
|
497
|
-
|
|
727
|
+
def published_catalog
|
|
728
|
+
models = published_store&.read
|
|
729
|
+
models unless models.nil? || models.empty?
|
|
730
|
+
rescue ModelRegistryError
|
|
731
|
+
nil
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def merge_discovered_models(published, remote_only:)
|
|
735
|
+
provider_fetch = self.class.fetch_provider_models(remote_only: remote_only)
|
|
736
|
+
self.class.log_provider_fetch(provider_fetch)
|
|
737
|
+
preserved = preserved_providers(provider_fetch, published)
|
|
738
|
+
preserved_models = all.select { |model| preserved.include?(model.provider) }
|
|
739
|
+
self.class.merge_models(provider_fetch[:models] + preserved_models, published)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
# A provider that answered replaces its own models and the published
|
|
743
|
+
# catalog replaces what it covers. Everything else survives the refresh,
|
|
744
|
+
# including the local providers a remote_only run never asks.
|
|
745
|
+
def preserved_providers(provider_fetch, published)
|
|
746
|
+
failed = provider_fetch[:failed].map { |failure| failure[:slug] }
|
|
747
|
+
covered = provider_fetch[:fetched_providers] + published.map(&:provider)
|
|
748
|
+
provider_gems = Provider.model_registry_files.keys.map(&:to_s)
|
|
749
|
+
failed | (all.map(&:provider).uniq - covered - provider_gems)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def persist_registry!(models, published:)
|
|
753
|
+
store = RubyLLM.config.model_registry_store
|
|
754
|
+
if store
|
|
755
|
+
raise ModelRegistryError, "Model registry store #{store.class} is read-only" unless store.respond_to?(:write)
|
|
498
756
|
|
|
757
|
+
store.write(self.class.new(models))
|
|
758
|
+
return
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
file = file_store
|
|
762
|
+
raise ModelRegistryError, 'No writable model registry store is configured' unless file
|
|
763
|
+
|
|
764
|
+
write_published_catalog(published)
|
|
765
|
+
file.write(models, etag: published.etag)
|
|
766
|
+
rescue ModelRegistryError
|
|
767
|
+
raise
|
|
768
|
+
rescue StandardError => e
|
|
769
|
+
destination = store_description(store) || file&.path || 'the configured store'
|
|
770
|
+
raise ModelRegistryError, "Could not save the model registry to #{destination}: #{e.message}"
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# A store keeps entries the merge dropped, such as the unlisted rows a
|
|
774
|
+
# Rails application still references, so its answer wins over the merge.
|
|
775
|
+
def stored_models
|
|
776
|
+
store = RubyLLM.config.model_registry_store
|
|
777
|
+
return unless store.respond_to?(:read)
|
|
778
|
+
|
|
779
|
+
models = Array(store.read)
|
|
780
|
+
models unless models.empty?
|
|
781
|
+
rescue StandardError => e
|
|
782
|
+
RubyLLM.logger.debug { "Could not re-read the model registry store: #{e.message}" }
|
|
783
|
+
nil
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def write_published_catalog(published)
|
|
787
|
+
published_store.write(published.models) unless published.not_modified
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def store_description(store)
|
|
791
|
+
return unless store
|
|
792
|
+
|
|
793
|
+
store.respond_to?(:description) ? store.description : store.class.name
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def find_with_provider(model_id, provider, config = nil)
|
|
797
|
+
resolved_id = Aliases.resolve(model_id, provider)
|
|
798
|
+
resolved_id = resolve_provider_registry_id(resolved_id, provider, config)
|
|
799
|
+
all_including_unlisted.find { |m| m.id == resolved_id && m.provider == provider.to_s } ||
|
|
800
|
+
all_including_unlisted.find { |m| m.id == model_id && m.provider == provider.to_s } ||
|
|
801
|
+
raise_model_not_found(model_id, provider: provider)
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def resolve_provider_registry_id(model_id, provider, config = nil)
|
|
805
|
+
provider_class = Provider.resolve(provider)
|
|
806
|
+
return model_id unless provider_class
|
|
807
|
+
|
|
808
|
+
provider_class.resolve_registry_id(model_id, self, config || RubyLLM.config)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# A name can be one provider's exact id and another's alias:
|
|
812
|
+
# claude-opus-4 is exact on vertexai, an alias on anthropic.
|
|
813
|
+
# Provider preference settles it, not the kind of match.
|
|
814
|
+
def find_without_provider(model_id)
|
|
499
815
|
resolved_id = Aliases.resolve(model_id)
|
|
500
|
-
|
|
501
|
-
|
|
816
|
+
matches = all_including_unlisted.select { |m| [model_id, resolved_id].include?(m.id) }
|
|
817
|
+
.sort_by { |m| m.id == model_id ? 0 : 1 }
|
|
818
|
+
|
|
819
|
+
preferred_match(matches) || raise_model_not_found(model_id)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def raise_model_not_found(model_id, provider: nil)
|
|
823
|
+
message = "Unknown model: #{model_id.inspect}"
|
|
824
|
+
message = "#{message} for provider: #{provider.inspect}" if provider
|
|
825
|
+
|
|
826
|
+
raise ModelNotFoundError, "#{message}. #{refresh_registry_guidance}"
|
|
827
|
+
end
|
|
502
828
|
|
|
503
|
-
|
|
829
|
+
def refresh_registry_guidance
|
|
830
|
+
'If the model exists at the provider, refresh the registry with `RubyLLM.models.refresh`.'
|
|
504
831
|
end
|
|
505
832
|
|
|
506
833
|
def preferred_match(candidates)
|
|
@@ -508,7 +835,7 @@ module RubyLLM
|
|
|
508
835
|
|
|
509
836
|
candidates.min_by do |model|
|
|
510
837
|
index = PROVIDER_PREFERENCE.index(model.provider)
|
|
511
|
-
index || PROVIDER_PREFERENCE.length
|
|
838
|
+
[model.unlisted? ? 1 : 0, index || PROVIDER_PREFERENCE.length]
|
|
512
839
|
end
|
|
513
840
|
end
|
|
514
841
|
end
|