ruby_llm 1.16.0 → 2.0.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rdoc_options +25 -0
- data/README.md +85 -32
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -111
- data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
- data/lib/ruby_llm/active_record/message_methods.rb +113 -136
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -151
- data/lib/ruby_llm/aliases.json +268 -100
- data/lib/ruby_llm/attachment.rb +187 -48
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1127 -198
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +261 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +121 -17
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +159 -23
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +153 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +24 -5
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +55 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +73261 -33733
- data/lib/ruby_llm/models.rb +477 -215
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
- data/lib/ruby_llm/protocols/anthropic.rb +101 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +560 -128
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +5 -14
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
- data/lib/ruby_llm/providers/bedrock.rb +216 -45
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
- data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +6 -18
- data/lib/ruby_llm/providers/mistral/models.rb +57 -23
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +16 -4
- data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +28 -20
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +160 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +3 -2
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -36
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +15 -5
- data/lib/ruby_llm/railtie.rb +7 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +138 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +380 -6
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +339 -97
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -41
- data/lib/ruby_llm/connection.rb +0 -159
- data/lib/ruby_llm/content.rb +0 -91
- data/lib/ruby_llm/deprecator.rb +0 -24
- data/lib/ruby_llm/error_middleware.rb +0 -81
- data/lib/ruby_llm/instrumentation.rb +0 -36
- data/lib/ruby_llm/mime_type.rb +0 -96
- data/lib/ruby_llm/model/info.rb +0 -164
- data/lib/ruby_llm/model_registry.rb +0 -39
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -236
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -218
- data/lib/ruby_llm/streaming.rb +0 -179
- data/lib/ruby_llm/tool_concurrency.rb +0 -105
- data/lib/ruby_llm/utils.rb +0 -130
- data/lib/tasks/models.rake +0 -593
- data/lib/tasks/release.rake +0 -94
- data/lib/tasks/vcr.rake +0 -124
data/lib/ruby_llm/models.rb
CHANGED
|
@@ -1,207 +1,237 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'date'
|
|
4
|
-
require 'json'
|
|
5
4
|
|
|
6
5
|
module RubyLLM
|
|
7
|
-
#
|
|
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.
|
|
8
17
|
class Models
|
|
9
18
|
include Enumerable
|
|
10
19
|
|
|
11
|
-
MODELS_DEV_PROVIDER_MAP = {
|
|
20
|
+
MODELS_DEV_PROVIDER_MAP = { # :nodoc:
|
|
12
21
|
'openai' => 'openai',
|
|
13
22
|
'anthropic' => 'anthropic',
|
|
14
23
|
'google' => 'gemini',
|
|
15
24
|
'google-vertex' => 'vertexai',
|
|
16
25
|
'amazon-bedrock' => 'bedrock',
|
|
26
|
+
'cohere' => 'cohere',
|
|
17
27
|
'deepseek' => 'deepseek',
|
|
18
28
|
'mistral' => 'mistral',
|
|
29
|
+
'ollama-cloud' => 'ollama_cloud',
|
|
19
30
|
'openrouter' => 'openrouter',
|
|
20
31
|
'perplexity' => 'perplexity',
|
|
32
|
+
'perplexity-agent' => 'perplexity',
|
|
21
33
|
'xai' => 'xai'
|
|
22
34
|
}.freeze
|
|
23
|
-
MODELS_DEV_INPUT_MODALITIES = %w[text image audio pdf video file].freeze
|
|
24
|
-
MODELS_DEV_OUTPUT_MODALITIES = %w[text image audio video embeddings moderation].freeze
|
|
25
|
-
|
|
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.
|
|
26
38
|
PROVIDER_PREFERENCE = %w[
|
|
27
39
|
openai
|
|
28
40
|
anthropic
|
|
29
41
|
gemini
|
|
30
|
-
vertexai
|
|
31
|
-
bedrock
|
|
32
|
-
openrouter
|
|
33
42
|
deepseek
|
|
34
43
|
mistral
|
|
44
|
+
cohere
|
|
35
45
|
perplexity
|
|
36
46
|
xai
|
|
47
|
+
vertexai
|
|
48
|
+
bedrock
|
|
49
|
+
openrouter
|
|
37
50
|
azure
|
|
51
|
+
ollama_cloud
|
|
38
52
|
ollama
|
|
39
53
|
gpustack
|
|
40
|
-
].freeze
|
|
54
|
+
].freeze # :nodoc:
|
|
41
55
|
INSTANCE_DELEGATES = (Enumerable.instance_methods(false) + %i[
|
|
42
56
|
all
|
|
43
57
|
each
|
|
44
58
|
find
|
|
59
|
+
listed
|
|
60
|
+
unlisted
|
|
45
61
|
chat_models
|
|
46
62
|
embedding_models
|
|
47
63
|
audio_models
|
|
48
64
|
image_models
|
|
49
65
|
by_family
|
|
50
66
|
by_provider
|
|
51
|
-
load_from_json
|
|
52
|
-
|
|
67
|
+
load_from_json
|
|
68
|
+
load_from_store
|
|
53
69
|
save_to_json
|
|
54
|
-
]).uniq.freeze
|
|
70
|
+
]).uniq.freeze # :nodoc:
|
|
55
71
|
|
|
56
72
|
class << self
|
|
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
|
+
|
|
57
78
|
INSTANCE_DELEGATES.each do |method_name|
|
|
58
79
|
define_method(method_name) do |*args, **kwargs, &block|
|
|
59
|
-
|
|
60
|
-
instance.public_send(method_name, *args, &block)
|
|
61
|
-
else
|
|
62
|
-
instance.public_send(method_name, *args, **kwargs, &block)
|
|
63
|
-
end
|
|
80
|
+
instance.public_send(method_name, *args, **kwargs, &block)
|
|
64
81
|
end
|
|
65
82
|
end
|
|
66
83
|
|
|
67
|
-
def instance
|
|
84
|
+
def instance # :nodoc:
|
|
68
85
|
@instance ||= new
|
|
69
86
|
end
|
|
70
87
|
|
|
71
|
-
def
|
|
72
|
-
File.expand_path('
|
|
88
|
+
def bundled_registry_file # :nodoc:
|
|
89
|
+
File.expand_path('models.json', __dir__)
|
|
73
90
|
end
|
|
74
91
|
|
|
75
|
-
def load_models
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
99
|
|
|
81
|
-
|
|
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 }
|
|
82
103
|
end
|
|
104
|
+
end
|
|
83
105
|
|
|
84
|
-
|
|
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
|
|
85
114
|
end
|
|
86
115
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
124
|
+
end
|
|
125
|
+
|
|
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
|
|
93
134
|
end
|
|
94
135
|
|
|
95
|
-
def
|
|
96
|
-
|
|
136
|
+
def fetch_published_registry(etag: nil) # :nodoc:
|
|
137
|
+
Registry::PublishedSource.new.fetch(etag:)
|
|
97
138
|
end
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
152
|
+
|
|
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:
|
|
102
156
|
RubyLLM.instrument('models.refresh.ruby_llm', remote_only:) do |payload|
|
|
103
|
-
existing_models =
|
|
157
|
+
existing_models = read_existing_models
|
|
104
158
|
|
|
105
159
|
provider_fetch = fetch_provider_models(remote_only: remote_only)
|
|
160
|
+
@last_provider_failures = provider_fetch[:failed]
|
|
106
161
|
log_provider_fetch(provider_fetch)
|
|
162
|
+
payload[:failed_providers] = provider_fetch[:failed].map { |failure| failure[:slug] }
|
|
107
163
|
|
|
108
164
|
models_dev_fetch = fetch_models_dev_models(existing_models)
|
|
109
165
|
log_models_dev_fetch(models_dev_fetch)
|
|
110
166
|
|
|
111
167
|
merged_models = merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
|
|
112
168
|
payload[:model_count] = merged_models.size
|
|
113
|
-
|
|
169
|
+
merged_models
|
|
114
170
|
end
|
|
115
171
|
end
|
|
116
172
|
|
|
117
|
-
def fetch_provider_models(remote_only: true) #
|
|
173
|
+
def fetch_provider_models(remote_only: true) # :nodoc:
|
|
118
174
|
config = RubyLLM.config
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Provider.configured_remote_providers(config)
|
|
122
|
-
else
|
|
123
|
-
Provider.configured_providers(config)
|
|
124
|
-
end
|
|
125
|
-
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) }
|
|
126
177
|
result = {
|
|
127
|
-
models: [],
|
|
128
|
-
fetched_providers: [],
|
|
129
|
-
configured_names: configured.map(&:name),
|
|
130
|
-
failed: []
|
|
178
|
+
models: [], fetched_providers: [], configured_names: providers.map(&:display_name), failed: [], empty: []
|
|
131
179
|
}
|
|
132
180
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
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)
|
|
139
187
|
result[:fetched_providers] << provider_class.slug
|
|
140
|
-
rescue StandardError => e
|
|
141
|
-
result[:failed] << { name: provider_class.name, slug: provider_class.slug, error: e }
|
|
142
188
|
end
|
|
189
|
+
rescue StandardError => e
|
|
190
|
+
result[:failed] << { name: provider_class.display_name, slug: provider_class.slug, error: e }
|
|
143
191
|
end
|
|
144
192
|
|
|
145
|
-
result[:fetched_providers].uniq!
|
|
146
193
|
result
|
|
147
194
|
end
|
|
148
195
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fetch_provider_models(remote_only: remote_only)[:models]
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
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
|
|
155
198
|
config ||= RubyLLM.config
|
|
156
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?
|
|
157
202
|
|
|
158
|
-
|
|
159
|
-
temp_instance = provider_class.new(config)
|
|
160
|
-
assume_exists = true if temp_instance.local? || temp_instance.assume_models_exist?
|
|
203
|
+
return [nil, provider_class.new(config)]
|
|
161
204
|
end
|
|
205
|
+
model_id ||= default_model
|
|
206
|
+
assume_model_exists = true if provider_class&.local? || provider_class&.assume_models_exist?
|
|
162
207
|
|
|
163
|
-
if
|
|
164
|
-
raise ArgumentError, 'Provider must be specified if
|
|
208
|
+
if assume_model_exists
|
|
209
|
+
raise ArgumentError, 'Provider must be specified if assume_model_exists is true' unless provider
|
|
165
210
|
|
|
166
|
-
provider_class ||=
|
|
167
|
-
provider_instance = provider_class.new(config)
|
|
211
|
+
provider_class ||= Provider.resolve!(provider)
|
|
168
212
|
|
|
169
|
-
model =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
end
|
|
175
|
-
end
|
|
213
|
+
model = begin
|
|
214
|
+
Models.find(model_id, provider: provider, config: config)
|
|
215
|
+
rescue ModelNotFoundError
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
176
218
|
|
|
177
|
-
model ||= Model
|
|
219
|
+
model ||= Model.default(model_id, provider_class.slug)
|
|
178
220
|
else
|
|
179
|
-
model = Models.find model_id, provider
|
|
180
|
-
provider_class = Provider.
|
|
181
|
-
provider_instance = provider_class.new(config)
|
|
221
|
+
model = Models.find model_id, provider: provider, config: config
|
|
222
|
+
provider_class = Provider.resolve!(model.provider)
|
|
182
223
|
end
|
|
183
|
-
[model,
|
|
224
|
+
[model, provider_class.new(config)]
|
|
184
225
|
end
|
|
185
226
|
|
|
186
|
-
def fetch_models_dev_models(existing_models) #
|
|
227
|
+
def fetch_models_dev_models(existing_models) # :nodoc:
|
|
187
228
|
RubyLLM.logger.info 'Fetching models from models.dev API...'
|
|
188
229
|
|
|
189
|
-
connection = Connection.basic do |f|
|
|
230
|
+
connection = Transport::Connection.basic do |f|
|
|
190
231
|
f.request :json
|
|
191
232
|
f.response :json, parser_options: { symbolize_names: true }
|
|
192
233
|
end
|
|
193
|
-
|
|
194
|
-
providers = response.body || {}
|
|
195
|
-
|
|
196
|
-
models = providers.flat_map do |provider_key, provider_data|
|
|
197
|
-
provider_slug = MODELS_DEV_PROVIDER_MAP[provider_key.to_s]
|
|
198
|
-
next [] unless provider_slug
|
|
199
|
-
|
|
200
|
-
(provider_data[:models] || {}).values.map do |model_data|
|
|
201
|
-
Model::Info.new(models_dev_model_to_info(model_data, provider_slug, provider_key.to_s))
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
{ 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 }
|
|
205
235
|
rescue StandardError => e
|
|
206
236
|
RubyLLM.logger.warn("Failed to fetch models.dev (#{e.class}: #{e.message}). Keeping existing.")
|
|
207
237
|
{
|
|
@@ -210,18 +240,33 @@ module RubyLLM
|
|
|
210
240
|
}
|
|
211
241
|
end
|
|
212
242
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
252
|
end
|
|
218
253
|
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
262
|
end
|
|
223
263
|
|
|
224
|
-
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:
|
|
225
270
|
RubyLLM.logger.info "Fetching models from providers: #{provider_fetch[:configured_names].join(', ')}"
|
|
226
271
|
provider_fetch[:failed].each do |failure|
|
|
227
272
|
RubyLLM.logger.warn(
|
|
@@ -229,15 +274,18 @@ module RubyLLM
|
|
|
229
274
|
'Keeping existing.'
|
|
230
275
|
)
|
|
231
276
|
end
|
|
277
|
+
Array(provider_fetch[:empty]).each do |provider|
|
|
278
|
+
RubyLLM.logger.warn("#{provider[:name]} listed no models. Keeping existing.")
|
|
279
|
+
end
|
|
232
280
|
end
|
|
233
281
|
|
|
234
|
-
def log_models_dev_fetch(models_dev_fetch)
|
|
282
|
+
def log_models_dev_fetch(models_dev_fetch) # :nodoc:
|
|
235
283
|
return if models_dev_fetch[:fetched]
|
|
236
284
|
|
|
237
285
|
RubyLLM.logger.warn('Using cached models.dev data due to fetch failure.')
|
|
238
286
|
end
|
|
239
287
|
|
|
240
|
-
def merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
|
|
288
|
+
def merge_with_existing(existing_models, provider_fetch, models_dev_fetch) # :nodoc:
|
|
241
289
|
existing_by_provider = existing_models.group_by(&:provider)
|
|
242
290
|
preserved_models = existing_by_provider
|
|
243
291
|
.except(*provider_fetch[:fetched_providers])
|
|
@@ -254,70 +302,50 @@ module RubyLLM
|
|
|
254
302
|
merge_models(provider_models, models_dev_models)
|
|
255
303
|
end
|
|
256
304
|
|
|
257
|
-
def merge_models(provider_models, models_dev_models)
|
|
305
|
+
def merge_models(provider_models, models_dev_models) # :nodoc:
|
|
258
306
|
models_dev_by_key = index_by_key(models_dev_models)
|
|
259
307
|
provider_by_key = index_by_key(provider_models)
|
|
308
|
+
provider_by_alias = index_provider_aliases(provider_models)
|
|
260
309
|
|
|
261
310
|
all_keys = models_dev_by_key.keys | provider_by_key.keys
|
|
262
311
|
|
|
263
312
|
models = all_keys.map do |key|
|
|
264
|
-
|
|
265
|
-
|
|
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)
|
|
266
315
|
|
|
267
316
|
if models_dev_model && provider_model
|
|
268
317
|
add_provider_metadata(models_dev_model, provider_model)
|
|
269
318
|
elsif models_dev_model
|
|
270
319
|
models_dev_model
|
|
271
320
|
else
|
|
272
|
-
provider_model
|
|
321
|
+
augment_model_capabilities(provider_model)
|
|
273
322
|
end
|
|
274
323
|
end
|
|
275
324
|
|
|
276
|
-
|
|
325
|
+
models.sort_by { |m| [m.provider, m.id] }
|
|
277
326
|
end
|
|
278
327
|
|
|
279
|
-
def
|
|
280
|
-
models.reject do |model|
|
|
281
|
-
model.provider.to_s == 'vertexai' && model.id.to_s.include?('/')
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def find_models_dev_model(key, models_dev_by_key)
|
|
286
|
-
# Direct match
|
|
328
|
+
def find_models_dev_model(key, models_dev_by_key, provider_model = nil) # :nodoc:
|
|
287
329
|
return models_dev_by_key[key] if models_dev_by_key[key]
|
|
288
330
|
|
|
289
331
|
provider, model_id = key.split(':', 2)
|
|
290
|
-
|
|
291
|
-
normalized_id = model_id.sub(/^[a-z]{2}\./, '')
|
|
292
|
-
context_override = nil
|
|
293
|
-
normalized_id = normalized_id.gsub(/:(\d+)k\b/) do
|
|
294
|
-
context_override = Regexp.last_match(1).to_i * 1000
|
|
295
|
-
''
|
|
296
|
-
end
|
|
297
|
-
bedrock_model = models_dev_by_key["bedrock:#{normalized_id}"]
|
|
298
|
-
if bedrock_model
|
|
299
|
-
data = bedrock_model.to_h.merge(id: model_id)
|
|
300
|
-
data[:context_window] = context_override if context_override
|
|
301
|
-
return Model::Info.new(data)
|
|
302
|
-
end
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# VertexAI uses same models as Gemini
|
|
306
|
-
return unless provider == 'vertexai'
|
|
307
|
-
|
|
308
|
-
gemini_model = models_dev_by_key["gemini:#{model_id}"]
|
|
309
|
-
return unless gemini_model
|
|
310
|
-
|
|
311
|
-
# Return Gemini's models.dev data but with VertexAI as provider
|
|
312
|
-
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)
|
|
313
333
|
end
|
|
314
334
|
|
|
315
|
-
def index_by_key(models)
|
|
335
|
+
def index_by_key(models) # :nodoc:
|
|
316
336
|
models.to_h do |model|
|
|
317
337
|
["#{model.provider}:#{model.id}", model]
|
|
318
338
|
end
|
|
319
339
|
end
|
|
320
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
|
+
|
|
321
349
|
def add_provider_metadata(models_dev_model, provider_model) # rubocop:disable Metrics/PerceivedComplexity
|
|
322
350
|
data = models_dev_model.to_h
|
|
323
351
|
data[:name] = provider_model.name if blank_value?(data[:name])
|
|
@@ -325,16 +353,51 @@ module RubyLLM
|
|
|
325
353
|
data[:created_at] = provider_model.created_at if blank_value?(data[:created_at])
|
|
326
354
|
data[:context_window] = provider_model.context_window if blank_value?(data[:context_window])
|
|
327
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])
|
|
328
357
|
data[:modalities] = provider_model.modalities.to_h if blank_value?(data[:modalities])
|
|
329
|
-
|
|
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)
|
|
330
362
|
data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
|
|
331
|
-
|
|
332
|
-
data[:capabilities] = (models_dev_model.capabilities + provider_capabilities).uniq
|
|
363
|
+
data[:capabilities] = merge_capabilities(models_dev_model, provider_model, data[:modalities])
|
|
333
364
|
normalize_embedding_modalities(data)
|
|
334
|
-
Model
|
|
365
|
+
Model.new(data)
|
|
335
366
|
end
|
|
336
367
|
|
|
337
|
-
def
|
|
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))
|
|
379
|
+
end
|
|
380
|
+
|
|
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:
|
|
338
401
|
return unless data[:id].to_s.include?('embedding')
|
|
339
402
|
|
|
340
403
|
modalities = data[:modalities].to_h
|
|
@@ -343,7 +406,7 @@ module RubyLLM
|
|
|
343
406
|
data[:modalities] = modalities
|
|
344
407
|
end
|
|
345
408
|
|
|
346
|
-
def blank_value?(value)
|
|
409
|
+
def blank_value?(value) # :nodoc:
|
|
347
410
|
return true if value.nil?
|
|
348
411
|
return value.empty? if value.is_a?(String) || value.is_a?(Array)
|
|
349
412
|
|
|
@@ -356,19 +419,19 @@ module RubyLLM
|
|
|
356
419
|
false
|
|
357
420
|
end
|
|
358
421
|
|
|
359
|
-
def
|
|
422
|
+
def models_dev_model_attributes(model_data, provider_slug, provider_key) # :nodoc:
|
|
360
423
|
modalities = normalize_models_dev_modalities(model_data[:modalities])
|
|
361
|
-
capabilities = models_dev_capabilities(model_data, modalities)
|
|
424
|
+
capabilities = models_dev_capabilities(model_data, modalities, provider_slug)
|
|
362
425
|
|
|
363
426
|
created_date = [model_data[:release_date], model_data[:last_updated]]
|
|
364
427
|
.find { |value| !value.to_s.strip.empty? }
|
|
365
428
|
|
|
366
429
|
data = {
|
|
367
|
-
id: model_data[:id],
|
|
430
|
+
id: models_dev_model_id(model_data[:id], provider_slug),
|
|
368
431
|
name: model_data[:name] || model_data[:id],
|
|
369
432
|
provider: provider_slug,
|
|
370
433
|
family: model_data[:family],
|
|
371
|
-
created_at: Utils.iso_date_prefix_to_utc_midnight_string(created_date),
|
|
434
|
+
created_at: Support::Utils.iso_date_prefix_to_utc_midnight_string(created_date),
|
|
372
435
|
context_window: model_data.dig(:limit, :context),
|
|
373
436
|
max_output_tokens: model_data.dig(:limit, :output),
|
|
374
437
|
knowledge_cutoff: normalize_models_dev_knowledge(model_data[:knowledge]),
|
|
@@ -382,16 +445,22 @@ module RubyLLM
|
|
|
382
445
|
data
|
|
383
446
|
end
|
|
384
447
|
|
|
385
|
-
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:
|
|
386
454
|
capabilities = []
|
|
387
455
|
capabilities << 'function_calling' if model_data[:tool_call]
|
|
388
456
|
capabilities << 'structured_output' if model_data[:structured_output]
|
|
389
457
|
capabilities << 'reasoning' if model_data[:reasoning] || model_data[:reasoning_options]
|
|
390
458
|
capabilities << 'vision' if modalities[:input].intersect?(%w[image video pdf])
|
|
391
|
-
capabilities.
|
|
459
|
+
capabilities << 'video' if modalities[:input].include?('video')
|
|
460
|
+
augment_capabilities(provider_slug, capabilities.uniq, model_data[:id], modalities)
|
|
392
461
|
end
|
|
393
462
|
|
|
394
|
-
def models_dev_pricing(cost)
|
|
463
|
+
def models_dev_pricing(cost) # :nodoc:
|
|
395
464
|
return {} unless cost
|
|
396
465
|
|
|
397
466
|
text_standard = {
|
|
@@ -408,12 +477,27 @@ module RubyLLM
|
|
|
408
477
|
}.compact
|
|
409
478
|
|
|
410
479
|
pricing = {}
|
|
411
|
-
|
|
480
|
+
text_tokens = models_dev_text_tokens_pricing(text_standard, cost)
|
|
481
|
+
pricing[:text_tokens] = text_tokens if text_tokens
|
|
412
482
|
pricing[:audio_tokens] = { standard: audio_standard } if audio_standard.any?
|
|
413
483
|
pricing
|
|
414
484
|
end
|
|
415
485
|
|
|
416
|
-
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:
|
|
417
501
|
metadata = {
|
|
418
502
|
source: 'models.dev',
|
|
419
503
|
provider_id: provider_key,
|
|
@@ -423,6 +507,9 @@ module RubyLLM
|
|
|
423
507
|
last_updated: model_data[:last_updated],
|
|
424
508
|
status: model_data[:status],
|
|
425
509
|
interleaved: model_data[:interleaved],
|
|
510
|
+
tool_call: model_data[:tool_call],
|
|
511
|
+
structured_output: model_data[:structured_output],
|
|
512
|
+
reasoning: model_data[:reasoning],
|
|
426
513
|
reasoning_options: model_data[:reasoning_options],
|
|
427
514
|
cost: model_data[:cost],
|
|
428
515
|
limit: model_data[:limit],
|
|
@@ -431,7 +518,7 @@ module RubyLLM
|
|
|
431
518
|
metadata.compact
|
|
432
519
|
end
|
|
433
520
|
|
|
434
|
-
def normalize_models_dev_modalities(modalities)
|
|
521
|
+
def normalize_models_dev_modalities(modalities) # :nodoc:
|
|
435
522
|
normalized = { input: [], output: [] }
|
|
436
523
|
return normalized unless modalities
|
|
437
524
|
|
|
@@ -440,7 +527,7 @@ module RubyLLM
|
|
|
440
527
|
normalized
|
|
441
528
|
end
|
|
442
529
|
|
|
443
|
-
def normalize_models_dev_knowledge(value)
|
|
530
|
+
def normalize_models_dev_knowledge(value) # :nodoc:
|
|
444
531
|
return if value.nil?
|
|
445
532
|
return value if value.is_a?(Date)
|
|
446
533
|
|
|
@@ -450,108 +537,286 @@ module RubyLLM
|
|
|
450
537
|
end
|
|
451
538
|
end
|
|
452
539
|
|
|
453
|
-
|
|
454
|
-
|
|
540
|
+
# :startdoc:
|
|
541
|
+
|
|
542
|
+
def initialize(models = nil) # :nodoc:
|
|
543
|
+
@models = models || self.class.load_models
|
|
455
544
|
end
|
|
456
545
|
|
|
457
|
-
# Replaces this registry
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
460
553
|
end
|
|
461
554
|
|
|
462
|
-
# Replaces this registry
|
|
463
|
-
#
|
|
464
|
-
def
|
|
465
|
-
|
|
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
|
|
466
563
|
end
|
|
467
564
|
|
|
468
|
-
#
|
|
469
|
-
# RubyLLM.
|
|
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
|
+
#
|
|
470
571
|
def save_to_json(file = RubyLLM.config.model_registry_file)
|
|
471
|
-
|
|
572
|
+
Registry::FileStore.new(file).write(all)
|
|
573
|
+
self
|
|
472
574
|
end
|
|
473
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.
|
|
474
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:
|
|
475
598
|
@models
|
|
476
599
|
end
|
|
477
600
|
|
|
601
|
+
# Yields each Model in the registry.
|
|
602
|
+
#
|
|
603
|
+
# RubyLLM.models.each { |model| puts model.id }
|
|
604
|
+
#
|
|
478
605
|
def each(&)
|
|
479
606
|
all.each(&)
|
|
480
607
|
end
|
|
481
608
|
|
|
482
|
-
|
|
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)
|
|
483
618
|
if provider
|
|
484
|
-
find_with_provider(model_id, provider)
|
|
619
|
+
find_with_provider(model_id, provider, config)
|
|
485
620
|
else
|
|
486
621
|
find_without_provider(model_id)
|
|
487
622
|
end
|
|
488
623
|
end
|
|
489
624
|
|
|
625
|
+
# Returns a new Models registry containing only chat models.
|
|
490
626
|
def chat_models
|
|
491
|
-
|
|
627
|
+
select_models { |m| m.type == :chat }
|
|
492
628
|
end
|
|
493
629
|
|
|
630
|
+
# Returns a new Models registry containing only embedding models.
|
|
494
631
|
def embedding_models
|
|
495
|
-
|
|
632
|
+
select_models { |m| m.type == :embedding || m.modalities.output.include?('embeddings') }
|
|
496
633
|
end
|
|
497
634
|
|
|
635
|
+
# Returns a new Models registry containing only models with audio
|
|
636
|
+
# output.
|
|
498
637
|
def audio_models
|
|
499
|
-
|
|
638
|
+
select_models { |m| m.type == :audio || m.modalities.output.include?('audio') }
|
|
500
639
|
end
|
|
501
640
|
|
|
641
|
+
# Returns a new Models registry containing only models with image
|
|
642
|
+
# output.
|
|
502
643
|
def image_models
|
|
503
|
-
|
|
644
|
+
select_models { |m| m.type == :image || m.modalities.output.include?('image') }
|
|
504
645
|
end
|
|
505
646
|
|
|
647
|
+
# Returns a new Models registry containing only models in +family+.
|
|
648
|
+
#
|
|
649
|
+
# RubyLLM.models.by_family('claude3_sonnet')
|
|
650
|
+
#
|
|
506
651
|
def by_family(family)
|
|
507
|
-
|
|
652
|
+
select_models { |m| m.family == family.to_s }
|
|
508
653
|
end
|
|
509
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
|
+
#
|
|
510
660
|
def by_provider(provider)
|
|
511
|
-
|
|
661
|
+
select_models { |m| m.provider == provider.to_s }
|
|
662
|
+
end
|
|
663
|
+
|
|
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
|
|
512
687
|
end
|
|
513
688
|
|
|
514
|
-
def
|
|
515
|
-
self.class.
|
|
689
|
+
def refresh_from_providers(remote_only: false) # :nodoc:
|
|
690
|
+
@models = self.class.fetch_merged_models(remote_only: remote_only)
|
|
691
|
+
self
|
|
516
692
|
end
|
|
517
693
|
|
|
518
|
-
def resolve(model_id, provider: nil,
|
|
519
|
-
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)
|
|
520
696
|
end
|
|
521
697
|
|
|
522
698
|
private
|
|
523
699
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
all.find { |m| m.id == model_id && m.provider == provider.to_s } ||
|
|
529
|
-
raise_model_not_found(model_id, 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(&))
|
|
530
704
|
end
|
|
531
705
|
|
|
532
|
-
def
|
|
533
|
-
|
|
534
|
-
return
|
|
706
|
+
def file_store
|
|
707
|
+
return if RubyLLM.config.model_registry_store
|
|
708
|
+
return unless RubyLLM.config.model_registry_file
|
|
535
709
|
|
|
536
|
-
|
|
537
|
-
|
|
710
|
+
Registry::FileStore.new(RubyLLM.config.model_registry_file)
|
|
711
|
+
end
|
|
538
712
|
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
541
719
|
|
|
542
|
-
|
|
543
|
-
|
|
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
|
|
544
725
|
end
|
|
545
726
|
|
|
546
|
-
def
|
|
547
|
-
|
|
548
|
-
|
|
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)
|
|
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
|
|
549
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)
|
|
550
815
|
resolved_id = Aliases.resolve(model_id)
|
|
551
|
-
|
|
552
|
-
|
|
816
|
+
matches = all_including_unlisted.select { |m| [model_id, resolved_id].include?(m.id) }
|
|
817
|
+
.sort_by { |m| m.id == model_id ? 0 : 1 }
|
|
553
818
|
|
|
554
|
-
raise_model_not_found(model_id)
|
|
819
|
+
preferred_match(matches) || raise_model_not_found(model_id)
|
|
555
820
|
end
|
|
556
821
|
|
|
557
822
|
def raise_model_not_found(model_id, provider: nil)
|
|
@@ -562,10 +827,7 @@ module RubyLLM
|
|
|
562
827
|
end
|
|
563
828
|
|
|
564
829
|
def refresh_registry_guidance
|
|
565
|
-
|
|
566
|
-
'If the model exists at the provider, refresh the registry with `RubyLLM.models.refresh!` ' \
|
|
567
|
-
'and persist it with `RubyLLM.models.save_to_json`. ' \
|
|
568
|
-
"Rails model registries can call `#{rails_model}.refresh!` instead."
|
|
830
|
+
'If the model exists at the provider, refresh the registry with `RubyLLM.models.refresh`.'
|
|
569
831
|
end
|
|
570
832
|
|
|
571
833
|
def preferred_match(candidates)
|
|
@@ -573,7 +835,7 @@ module RubyLLM
|
|
|
573
835
|
|
|
574
836
|
candidates.min_by do |model|
|
|
575
837
|
index = PROVIDER_PREFERENCE.index(model.provider)
|
|
576
|
-
index || PROVIDER_PREFERENCE.length
|
|
838
|
+
[model.unlisted? ? 1 : 0, index || PROVIDER_PREFERENCE.length]
|
|
577
839
|
end
|
|
578
840
|
end
|
|
579
841
|
end
|