dify_llm 1.9.2 → 1.14.1

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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +27 -8
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
  51. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
  52. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
  53. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
  54. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
  55. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  56. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  57. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  59. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  60. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  61. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  64. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  65. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  66. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  67. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  68. data/lib/ruby_llm/active_record/acts_as.rb +10 -4
  69. data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
  70. data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
  71. data/lib/ruby_llm/active_record/message_methods.rb +58 -8
  72. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  73. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  74. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  75. data/lib/ruby_llm/agent.rb +365 -0
  76. data/lib/ruby_llm/aliases.json +106 -61
  77. data/lib/ruby_llm/attachment.rb +8 -3
  78. data/lib/ruby_llm/chat.rb +150 -22
  79. data/lib/ruby_llm/configuration.rb +65 -65
  80. data/lib/ruby_llm/connection.rb +11 -7
  81. data/lib/ruby_llm/content.rb +6 -2
  82. data/lib/ruby_llm/error.rb +37 -1
  83. data/lib/ruby_llm/message.rb +43 -15
  84. data/lib/ruby_llm/model/info.rb +15 -13
  85. data/lib/ruby_llm/models.json +25039 -12260
  86. data/lib/ruby_llm/models.rb +185 -24
  87. data/lib/ruby_llm/provider.rb +26 -4
  88. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
  89. data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
  90. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  91. data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
  92. data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
  93. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
  94. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  95. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  96. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  97. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  98. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  99. data/lib/ruby_llm/providers/azure.rb +148 -0
  100. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  101. data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
  102. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  103. data/lib/ruby_llm/providers/bedrock/models.rb +104 -65
  104. data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
  105. data/lib/ruby_llm/providers/bedrock.rb +69 -52
  106. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
  107. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  108. data/lib/ruby_llm/providers/dify/chat.rb +82 -7
  109. data/lib/ruby_llm/providers/dify/media.rb +2 -2
  110. data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
  111. data/lib/ruby_llm/providers/dify.rb +4 -0
  112. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
  113. data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
  114. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  115. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  116. data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
  117. data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
  118. data/lib/ruby_llm/providers/gemini.rb +4 -0
  119. data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
  120. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  121. data/lib/ruby_llm/providers/gpustack.rb +8 -0
  122. data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
  123. data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
  124. data/lib/ruby_llm/providers/mistral.rb +4 -0
  125. data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
  126. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  127. data/lib/ruby_llm/providers/ollama.rb +11 -1
  128. data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
  129. data/lib/ruby_llm/providers/openai/chat.rb +101 -7
  130. data/lib/ruby_llm/providers/openai/media.rb +5 -2
  131. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  132. data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
  133. data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
  134. data/lib/ruby_llm/providers/openai/tools.rb +27 -2
  135. data/lib/ruby_llm/providers/openai.rb +11 -1
  136. data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
  137. data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
  138. data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
  139. data/lib/ruby_llm/providers/openrouter.rb +37 -1
  140. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  141. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  142. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  143. data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
  144. data/lib/ruby_llm/providers/vertexai.rb +23 -7
  145. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  146. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  147. data/lib/ruby_llm/providers/xai.rb +32 -0
  148. data/lib/ruby_llm/stream_accumulator.rb +120 -18
  149. data/lib/ruby_llm/streaming.rb +60 -57
  150. data/lib/ruby_llm/thinking.rb +49 -0
  151. data/lib/ruby_llm/tokens.rb +47 -0
  152. data/lib/ruby_llm/tool.rb +48 -3
  153. data/lib/ruby_llm/tool_call.rb +6 -3
  154. data/lib/ruby_llm/version.rb +1 -1
  155. data/lib/ruby_llm.rb +14 -8
  156. data/lib/tasks/models.rake +61 -22
  157. data/lib/tasks/release.rake +1 -1
  158. data/lib/tasks/ruby_llm.rake +9 -1
  159. data/lib/tasks/vcr.rake +33 -1
  160. metadata +67 -16
  161. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
  162. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  163. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  164. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  165. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
  166. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  167. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
  168. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
@@ -16,6 +16,21 @@ module RubyLLM
16
16
  'openrouter' => 'openrouter',
17
17
  'perplexity' => 'perplexity'
18
18
  }.freeze
19
+ PROVIDER_PREFERENCE = %w[
20
+ openai
21
+ anthropic
22
+ gemini
23
+ vertexai
24
+ bedrock
25
+ openrouter
26
+ deepseek
27
+ mistral
28
+ perplexity
29
+ xai
30
+ azure
31
+ ollama
32
+ gpustack
33
+ ].freeze
19
34
 
20
35
  class << self
21
36
  def instance
@@ -32,30 +47,60 @@ module RubyLLM
32
47
 
33
48
  def read_from_json(file = RubyLLM.config.model_registry_file)
34
49
  data = File.exist?(file) ? File.read(file) : '[]'
35
- JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
50
+ models = JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
51
+ filter_models(models)
36
52
  rescue JSON::ParserError
37
53
  []
38
54
  end
39
55
 
40
56
  def refresh!(remote_only: false)
41
- provider_models = fetch_from_providers(remote_only: remote_only)
42
- models_dev_models = fetch_from_models_dev
43
- merged_models = merge_models(provider_models, models_dev_models)
57
+ existing_models = load_existing_models
58
+
59
+ provider_fetch = fetch_provider_models(remote_only: remote_only)
60
+ log_provider_fetch(provider_fetch)
61
+
62
+ models_dev_fetch = fetch_models_dev_models(existing_models)
63
+ log_models_dev_fetch(models_dev_fetch)
64
+
65
+ merged_models = merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
44
66
  @instance = new(merged_models)
45
67
  end
46
68
 
47
- def fetch_from_providers(remote_only: true)
69
+ def fetch_provider_models(remote_only: true) # rubocop:disable Metrics/PerceivedComplexity
48
70
  config = RubyLLM.config
71
+ provider_classes = remote_only ? Provider.remote_providers.values : Provider.providers.values
49
72
  configured_classes = if remote_only
50
73
  Provider.configured_remote_providers(config)
51
74
  else
52
75
  Provider.configured_providers(config)
53
76
  end
54
- configured = configured_classes.map { |klass| klass.new(config) }
77
+ configured = configured_classes.select { |klass| provider_classes.include?(klass) }
78
+ result = {
79
+ models: [],
80
+ fetched_providers: [],
81
+ configured_names: configured.map(&:name),
82
+ failed: []
83
+ }
55
84
 
56
- RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:name).join(', ')}"
85
+ provider_classes.each do |provider_class|
86
+ next if remote_only && provider_class.local?
87
+ next unless provider_class.configured?(config)
57
88
 
58
- configured.flat_map(&:list_models)
89
+ begin
90
+ result[:models].concat(provider_class.new(config).list_models)
91
+ result[:fetched_providers] << provider_class.slug
92
+ rescue StandardError => e
93
+ result[:failed] << { name: provider_class.name, slug: provider_class.slug, error: e }
94
+ end
95
+ end
96
+
97
+ result[:fetched_providers].uniq!
98
+ result
99
+ end
100
+
101
+ # Backwards-compatible wrapper used by specs.
102
+ def fetch_from_providers(remote_only: true)
103
+ fetch_provider_models(remote_only: remote_only)[:models]
59
104
  end
60
105
 
61
106
  def resolve(model_id, provider: nil, assume_exists: false, config: nil) # rubocop:disable Metrics/PerceivedComplexity
@@ -64,7 +109,7 @@ module RubyLLM
64
109
 
65
110
  if provider_class
66
111
  temp_instance = provider_class.new(config)
67
- assume_exists = true if temp_instance.local?
112
+ assume_exists = true if temp_instance.local? || temp_instance.assume_models_exist?
68
113
  end
69
114
 
70
115
  if assume_exists
@@ -103,7 +148,7 @@ module RubyLLM
103
148
  instance.respond_to?(method, include_private) || super
104
149
  end
105
150
 
106
- def fetch_from_models_dev
151
+ def fetch_models_dev_models(existing_models) # rubocop:disable Metrics/PerceivedComplexity
107
152
  RubyLLM.logger.info 'Fetching models from models.dev API...'
108
153
 
109
154
  connection = Connection.basic do |f|
@@ -121,7 +166,52 @@ module RubyLLM
121
166
  Model::Info.new(models_dev_model_to_info(model_data, provider_slug, provider_key.to_s))
122
167
  end
123
168
  end
124
- models.reject { |model| model.provider.nil? || model.id.nil? }
169
+ { models: models.reject { |model| model.provider.nil? || model.id.nil? }, fetched: true }
170
+ rescue StandardError => e
171
+ RubyLLM.logger.warn("Failed to fetch models.dev (#{e.class}: #{e.message}). Keeping existing.")
172
+ {
173
+ models: existing_models.select { |model| model.metadata[:source] == 'models.dev' },
174
+ fetched: false
175
+ }
176
+ end
177
+
178
+ def load_existing_models
179
+ existing_models = instance&.all
180
+ existing_models = read_from_json if existing_models.nil? || existing_models.empty?
181
+ existing_models
182
+ end
183
+
184
+ def log_provider_fetch(provider_fetch)
185
+ RubyLLM.logger.info "Fetching models from providers: #{provider_fetch[:configured_names].join(', ')}"
186
+ provider_fetch[:failed].each do |failure|
187
+ RubyLLM.logger.warn(
188
+ "Failed to fetch #{failure[:name]} models (#{failure[:error].class}: #{failure[:error].message}). " \
189
+ 'Keeping existing.'
190
+ )
191
+ end
192
+ end
193
+
194
+ def log_models_dev_fetch(models_dev_fetch)
195
+ return if models_dev_fetch[:fetched]
196
+
197
+ RubyLLM.logger.warn('Using cached models.dev data due to fetch failure.')
198
+ end
199
+
200
+ def merge_with_existing(existing_models, provider_fetch, models_dev_fetch)
201
+ existing_by_provider = existing_models.group_by(&:provider)
202
+ preserved_models = existing_by_provider
203
+ .except(*provider_fetch[:fetched_providers])
204
+ .values
205
+ .flatten
206
+
207
+ provider_models = provider_fetch[:models] + preserved_models
208
+ models_dev_models = if models_dev_fetch[:fetched]
209
+ models_dev_fetch[:models]
210
+ else
211
+ existing_models.select { |model| model.metadata[:source] == 'models.dev' }
212
+ end
213
+
214
+ merge_models(provider_models, models_dev_models)
125
215
  end
126
216
 
127
217
  def merge_models(provider_models, models_dev_models)
@@ -143,15 +233,36 @@ module RubyLLM
143
233
  end
144
234
  end
145
235
 
146
- models.sort_by { |m| [m.provider, m.id] }
236
+ filter_models(models).sort_by { |m| [m.provider, m.id] }
237
+ end
238
+
239
+ def filter_models(models)
240
+ models.reject do |model|
241
+ model.provider.to_s == 'vertexai' && model.id.to_s.include?('/')
242
+ end
147
243
  end
148
244
 
149
245
  def find_models_dev_model(key, models_dev_by_key)
150
246
  # Direct match
151
247
  return models_dev_by_key[key] if models_dev_by_key[key]
152
248
 
153
- # VertexAI uses same models as Gemini
154
249
  provider, model_id = key.split(':', 2)
250
+ if provider == 'bedrock'
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
155
266
  return unless provider == 'vertexai'
156
267
 
157
268
  gemini_model = models_dev_by_key["gemini:#{model_id}"]
@@ -162,28 +273,61 @@ module RubyLLM
162
273
  end
163
274
 
164
275
  def index_by_key(models)
165
- models.each_with_object({}) do |model, hash|
166
- hash["#{model.provider}:#{model.id}"] = model
276
+ models.to_h do |model|
277
+ ["#{model.provider}:#{model.id}", model]
167
278
  end
168
279
  end
169
280
 
170
- def add_provider_metadata(models_dev_model, provider_model)
281
+ def add_provider_metadata(models_dev_model, provider_model) # rubocop:disable Metrics/PerceivedComplexity
171
282
  data = models_dev_model.to_h
283
+ data[:name] = provider_model.name if blank_value?(data[:name])
284
+ data[:family] = provider_model.family if blank_value?(data[:family])
285
+ data[:created_at] = provider_model.created_at if blank_value?(data[:created_at])
286
+ data[:context_window] = provider_model.context_window if blank_value?(data[:context_window])
287
+ data[:max_output_tokens] = provider_model.max_output_tokens if blank_value?(data[:max_output_tokens])
288
+ data[:modalities] = provider_model.modalities.to_h if blank_value?(data[:modalities])
289
+ data[:pricing] = provider_model.pricing.to_h if blank_value?(data[:pricing])
172
290
  data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
173
291
  data[:capabilities] = (models_dev_model.capabilities + provider_model.capabilities).uniq
292
+ normalize_embedding_modalities(data)
174
293
  Model::Info.new(data)
175
294
  end
176
295
 
296
+ def normalize_embedding_modalities(data)
297
+ return unless data[:id].to_s.include?('embedding')
298
+
299
+ modalities = data[:modalities].to_h
300
+ modalities[:input] = ['text'] if modalities[:input].nil? || modalities[:input].empty?
301
+ modalities[:output] = ['embeddings']
302
+ data[:modalities] = modalities
303
+ end
304
+
305
+ def blank_value?(value)
306
+ return true if value.nil?
307
+ return value.empty? if value.is_a?(String) || value.is_a?(Array)
308
+
309
+ if value.is_a?(Hash)
310
+ return true if value.empty?
311
+
312
+ return value.values.all? { |nested| blank_value?(nested) }
313
+ end
314
+
315
+ false
316
+ end
317
+
177
318
  def models_dev_model_to_info(model_data, provider_slug, provider_key)
178
319
  modalities = normalize_models_dev_modalities(model_data[:modalities])
179
320
  capabilities = models_dev_capabilities(model_data, modalities)
180
321
 
181
- {
322
+ created_date = [model_data[:release_date], model_data[:last_updated]]
323
+ .find { |value| !value.to_s.strip.empty? }
324
+
325
+ data = {
182
326
  id: model_data[:id],
183
327
  name: model_data[:name] || model_data[:id],
184
328
  provider: provider_slug,
185
329
  family: model_data[:family],
186
- created_at: model_data[:release_date] || model_data[:last_updated],
330
+ created_at: created_date ? "#{created_date} 00:00:00 UTC" : nil,
187
331
  context_window: model_data.dig(:limit, :context),
188
332
  max_output_tokens: model_data.dig(:limit, :output),
189
333
  knowledge_cutoff: normalize_models_dev_knowledge(model_data[:knowledge]),
@@ -192,6 +336,9 @@ module RubyLLM
192
336
  pricing: models_dev_pricing(model_data[:cost]),
193
337
  metadata: models_dev_metadata(model_data, provider_key)
194
338
  }
339
+
340
+ normalize_embedding_modalities(data)
341
+ data
195
342
  end
196
343
 
197
344
  def models_dev_capabilities(model_data, modalities)
@@ -261,7 +408,7 @@ module RubyLLM
261
408
  end
262
409
 
263
410
  def initialize(models = nil)
264
- @models = models || self.class.load_models
411
+ @models = self.class.filter_models(models || self.class.load_models)
265
412
  end
266
413
 
267
414
  def load_from_json!(file = RubyLLM.config.model_registry_file)
@@ -325,8 +472,8 @@ module RubyLLM
325
472
  def find_with_provider(model_id, provider)
326
473
  resolved_id = Aliases.resolve(model_id, provider)
327
474
  resolved_id = resolve_bedrock_region_id(resolved_id) if provider.to_s == 'bedrock'
328
- all.find { |m| m.id == model_id && m.provider == provider.to_s } ||
329
- all.find { |m| m.id == resolved_id && m.provider == provider.to_s } ||
475
+ all.find { |m| m.id == resolved_id && m.provider == provider.to_s } ||
476
+ all.find { |m| m.id == model_id && m.provider == provider.to_s } ||
330
477
  raise(ModelNotFoundError, "Unknown model: #{model_id} for provider: #{provider}")
331
478
  end
332
479
 
@@ -345,9 +492,23 @@ module RubyLLM
345
492
  end
346
493
 
347
494
  def find_without_provider(model_id)
348
- all.find { |m| m.id == model_id } ||
349
- all.find { |m| m.id == Aliases.resolve(model_id) } ||
350
- raise(ModelNotFoundError, "Unknown model: #{model_id}")
495
+ exact_matches = all.select { |m| m.id == model_id }
496
+ return preferred_match(exact_matches) if exact_matches.any?
497
+
498
+ resolved_id = Aliases.resolve(model_id)
499
+ alias_matches = all.select { |m| m.id == resolved_id }
500
+ return preferred_match(alias_matches) if alias_matches.any?
501
+
502
+ raise(ModelNotFoundError, "Unknown model: #{model_id}")
503
+ end
504
+
505
+ def preferred_match(candidates)
506
+ return candidates.first if candidates.size == 1
507
+
508
+ candidates.min_by do |model|
509
+ index = PROVIDER_PREFERENCE.index(model.provider)
510
+ index || PROVIDER_PREFERENCE.length
511
+ end
351
512
  end
352
513
  end
353
514
  end
@@ -37,17 +37,21 @@ module RubyLLM
37
37
  self.class.configuration_requirements
38
38
  end
39
39
 
40
- def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists
40
+ # rubocop:disable Metrics/ParameterLists
41
+ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
42
+ tool_prefs: nil, &)
41
43
  normalized_temperature = maybe_normalize_temperature(temperature, model)
42
44
 
43
45
  payload = Utils.deep_merge(
44
46
  render_payload(
45
47
  messages,
46
48
  tools: tools,
49
+ tool_prefs: tool_prefs,
47
50
  temperature: normalized_temperature,
48
51
  model: model,
49
52
  stream: block_given?,
50
- schema: schema
53
+ schema: schema,
54
+ thinking: thinking
51
55
  ),
52
56
  params
53
57
  )
@@ -58,6 +62,7 @@ module RubyLLM
58
62
  sync_response @connection, payload, headers
59
63
  end
60
64
  end
65
+ # rubocop:enable Metrics/ParameterLists
61
66
 
62
67
  def list_models
63
68
  response = @connection.get models_url
@@ -101,16 +106,24 @@ module RubyLLM
101
106
  self.class.remote?
102
107
  end
103
108
 
109
+ def assume_models_exist?
110
+ self.class.assume_models_exist?
111
+ end
112
+
104
113
  def parse_error(response)
105
114
  return if response.body.empty?
106
115
 
107
116
  body = try_parse_json(response.body)
108
117
  case body
109
118
  when Hash
119
+ error = body['error']
120
+ return error if error.is_a?(String)
121
+
110
122
  body.dig('error', 'message')
111
123
  when Array
112
124
  body.map do |part|
113
- part.dig('error', 'message')
125
+ error = part['error']
126
+ error.is_a?(String) ? error : part.dig('error', 'message')
114
127
  end.join('. ')
115
128
  else
116
129
  body
@@ -144,13 +157,17 @@ module RubyLLM
144
157
  end
145
158
 
146
159
  def capabilities
147
- raise NotImplementedError
160
+ nil
148
161
  end
149
162
 
150
163
  def configuration_requirements
151
164
  []
152
165
  end
153
166
 
167
+ def configuration_options
168
+ []
169
+ end
170
+
154
171
  def local?
155
172
  false
156
173
  end
@@ -159,12 +176,17 @@ module RubyLLM
159
176
  !local?
160
177
  end
161
178
 
179
+ def assume_models_exist?
180
+ false
181
+ end
182
+
162
183
  def configured?(config)
163
184
  configuration_requirements.all? { |req| config.send(req) }
164
185
  end
165
186
 
166
187
  def register(name, provider_class)
167
188
  providers[name.to_sym] = provider_class
189
+ RubyLLM::Configuration.register_provider_options(provider_class.configuration_options)
168
190
  end
169
191
 
170
192
  def resolve(name)
@@ -3,130 +3,16 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Anthropic
6
- # Determines capabilities and pricing for Anthropic models
6
+ # Provider-level capability checks used outside the model registry.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def determine_context_window(_model_id)
11
- 200_000
10
+ def supports_tool_choice?(_model_id)
11
+ true
12
12
  end
13
13
 
14
- def determine_max_tokens(model_id)
15
- case model_id
16
- when /claude-3-7-sonnet/, /claude-3-5/ then 8_192
17
- else 4_096
18
- end
19
- end
20
-
21
- def get_input_price(model_id)
22
- PRICES.dig(model_family(model_id), :input) || default_input_price
23
- end
24
-
25
- def get_output_price(model_id)
26
- PRICES.dig(model_family(model_id), :output) || default_output_price
27
- end
28
-
29
- def supports_vision?(model_id)
30
- !model_id.match?(/claude-[12]/)
31
- end
32
-
33
- def supports_functions?(model_id)
34
- model_id.match?(/claude-3/)
35
- end
36
-
37
- def supports_json_mode?(model_id)
38
- model_id.match?(/claude-3/)
39
- end
40
-
41
- def supports_extended_thinking?(model_id)
42
- model_id.match?(/claude-3-7-sonnet/)
43
- end
44
-
45
- def model_family(model_id)
46
- case model_id
47
- when /claude-3-7-sonnet/ then 'claude-3-7-sonnet'
48
- when /claude-3-5-sonnet/ then 'claude-3-5-sonnet'
49
- when /claude-3-5-haiku/ then 'claude-3-5-haiku'
50
- when /claude-3-opus/ then 'claude-3-opus'
51
- when /claude-3-sonnet/ then 'claude-3-sonnet'
52
- when /claude-3-haiku/ then 'claude-3-haiku'
53
- else 'claude-2'
54
- end
55
- end
56
-
57
- def model_type(_)
58
- 'chat'
59
- end
60
-
61
- PRICES = {
62
- 'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
63
- 'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
64
- 'claude-3-5-haiku': { input: 0.80, output: 4.0 },
65
- 'claude-3-opus': { input: 15.0, output: 75.0 },
66
- 'claude-3-haiku': { input: 0.25, output: 1.25 },
67
- 'claude-2': { input: 3.0, output: 15.0 }
68
- }.freeze
69
-
70
- def default_input_price
71
- 3.0
72
- end
73
-
74
- def default_output_price
75
- 15.0
76
- end
77
-
78
- def modalities_for(model_id)
79
- modalities = {
80
- input: ['text'],
81
- output: ['text']
82
- }
83
-
84
- unless model_id.match?(/claude-[12]/)
85
- modalities[:input] << 'image'
86
- modalities[:input] << 'pdf'
87
- end
88
-
89
- modalities
90
- end
91
-
92
- def capabilities_for(model_id)
93
- capabilities = ['streaming']
94
-
95
- if model_id.match?(/claude-3/)
96
- capabilities << 'function_calling'
97
- capabilities << 'batch'
98
- end
99
-
100
- capabilities << 'reasoning' if model_id.match?(/claude-3-7|-4/)
101
- capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
102
- capabilities
103
- end
104
-
105
- def pricing_for(model_id)
106
- family = model_family(model_id)
107
- prices = PRICES.fetch(family.to_sym, { input: default_input_price, output: default_output_price })
108
-
109
- standard_pricing = {
110
- input_per_million: prices[:input],
111
- output_per_million: prices[:output]
112
- }
113
-
114
- batch_pricing = {
115
- input_per_million: prices[:input] * 0.5,
116
- output_per_million: prices[:output] * 0.5
117
- }
118
-
119
- if model_id.match?(/claude-3-7/)
120
- standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5
121
- batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25
122
- end
123
-
124
- {
125
- text_tokens: {
126
- standard: standard_pricing,
127
- batch: batch_pricing
128
- }
129
- }
14
+ def supports_tool_parallel_control?(_model_id)
15
+ true
130
16
  end
131
17
  end
132
18
  end