ruby_llm 1.13.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -7
  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 +1 -1
  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 +33 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
  51. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
  52. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
  53. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  54. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  55. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  56. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  57. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  59. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  60. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
  61. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  64. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  65. data/lib/ruby_llm/active_record/acts_as.rb +2 -0
  66. data/lib/ruby_llm/active_record/acts_as_legacy.rb +2 -0
  67. data/lib/ruby_llm/active_record/chat_methods.rb +13 -6
  68. data/lib/ruby_llm/active_record/message_methods.rb +17 -0
  69. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  70. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  71. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  72. data/lib/ruby_llm/agent.rb +11 -0
  73. data/lib/ruby_llm/aliases.json +34 -15
  74. data/lib/ruby_llm/attachment.rb +3 -0
  75. data/lib/ruby_llm/configuration.rb +54 -73
  76. data/lib/ruby_llm/connection.rb +1 -3
  77. data/lib/ruby_llm/error.rb +5 -0
  78. data/lib/ruby_llm/model/info.rb +14 -12
  79. data/lib/ruby_llm/models.json +7446 -10126
  80. data/lib/ruby_llm/models.rb +10 -3
  81. data/lib/ruby_llm/provider.rb +5 -0
  82. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
  83. data/lib/ruby_llm/providers/anthropic/models.rb +2 -8
  84. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  85. data/lib/ruby_llm/providers/azure.rb +4 -0
  86. data/lib/ruby_llm/providers/bedrock.rb +4 -0
  87. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -119
  88. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  89. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
  90. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  91. data/lib/ruby_llm/providers/gemini.rb +4 -0
  92. data/lib/ruby_llm/providers/gpustack.rb +4 -0
  93. data/lib/ruby_llm/providers/mistral.rb +4 -0
  94. data/lib/ruby_llm/providers/ollama.rb +4 -0
  95. data/lib/ruby_llm/providers/openai/capabilities.rb +95 -203
  96. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  97. data/lib/ruby_llm/providers/openai.rb +10 -0
  98. data/lib/ruby_llm/providers/openrouter/images.rb +1 -1
  99. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  100. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  101. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  102. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  103. data/lib/ruby_llm/providers/vertexai.rb +4 -0
  104. data/lib/ruby_llm/providers/xai.rb +4 -0
  105. data/lib/ruby_llm/version.rb +1 -1
  106. data/lib/tasks/release.rake +1 -1
  107. data/lib/tasks/ruby_llm.rake +6 -5
  108. data/lib/tasks/vcr.rake +1 -1
  109. metadata +49 -11
  110. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
@@ -3,13 +3,11 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAI
6
- # Determines capabilities and pricing for OpenAI models
6
+ # Provider-level capability checks and narrow registry fallbacks.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
10
  MODEL_PATTERNS = {
11
- dall_e: /^dall-e/,
12
- chatgpt4o: /^chatgpt-4o/,
13
11
  gpt41: /^gpt-4\.1(?!-(?:mini|nano))/,
14
12
  gpt41_mini: /^gpt-4\.1-mini/,
15
13
  gpt41_nano: /^gpt-4\.1-nano/,
@@ -26,9 +24,9 @@ module RubyLLM
26
24
  gpt4o_realtime: /^gpt-4o-realtime/,
27
25
  gpt4o_search: /^gpt-4o-search/,
28
26
  gpt4o_transcribe: /^gpt-4o-transcribe/,
29
- gpt5: /^gpt-5/,
30
- gpt5_mini: /^gpt-5-mini/,
31
- gpt5_nano: /^gpt-5-nano/,
27
+ gpt5: /^gpt-5(?!.*(?:mini|nano))/,
28
+ gpt5_mini: /^gpt-5.*mini/,
29
+ gpt5_nano: /^gpt-5.*nano/,
32
30
  o1: /^o1(?!-(?:mini|pro))/,
33
31
  o1_mini: /^o1-mini/,
34
32
  o1_pro: /^o1-pro/,
@@ -44,79 +42,6 @@ module RubyLLM
44
42
  moderation: /^(?:omni|text)-moderation/
45
43
  }.freeze
46
44
 
47
- def context_window_for(model_id)
48
- case model_family(model_id)
49
- when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
50
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'chatgpt4o', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
51
- 'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime',
52
- 'gpt4o_search', 'gpt4o_transcribe', 'gpt4o_mini_search', 'o1_mini' then 128_000
53
- when 'gpt4' then 8_192
54
- when 'gpt4o_mini_transcribe' then 16_000
55
- when 'o1', 'o1_pro', 'o3_mini' then 200_000
56
- when 'gpt35_turbo' then 16_385
57
- when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
58
- 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
59
- else 4_096
60
- end
61
- end
62
-
63
- def max_tokens_for(model_id)
64
- case model_family(model_id)
65
- when 'gpt5', 'gpt5_mini', 'gpt5_nano' then 400_000
66
- when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
67
- when 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'gpt4o_mini_search' then 16_384
68
- when 'babbage', 'davinci' then 16_384 # rubocop:disable Lint/DuplicateBranch
69
- when 'gpt4' then 8_192
70
- when 'gpt35_turbo' then 4_096
71
- when 'gpt4_turbo', 'gpt4o_realtime', 'gpt4o_mini_realtime' then 4_096 # rubocop:disable Lint/DuplicateBranch
72
- when 'gpt4o_mini_transcribe' then 2_000
73
- when 'o1', 'o1_pro', 'o3_mini' then 100_000
74
- when 'o1_mini' then 65_536
75
- when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
76
- 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
77
- else 16_384 # rubocop:disable Lint/DuplicateBranch
78
- end
79
- end
80
-
81
- def supports_vision?(model_id)
82
- case model_family(model_id)
83
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4',
84
- 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'moderation', 'gpt4o_search',
85
- 'gpt4o_mini_search' then true
86
- else false
87
- end
88
- end
89
-
90
- def supports_functions?(model_id)
91
- case model_family(model_id)
92
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o',
93
- 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini' then true
94
- when 'chatgpt4o', 'gpt35_turbo', 'o1_mini', 'gpt4o_mini_tts',
95
- 'gpt4o_transcribe', 'gpt4o_search', 'gpt4o_mini_search' then false
96
- else false # rubocop:disable Lint/DuplicateBranch
97
- end
98
- end
99
-
100
- def supports_tool_choice?(_model_id)
101
- true
102
- end
103
-
104
- def supports_tool_parallel_control?(_model_id)
105
- true
106
- end
107
-
108
- def supports_structured_output?(model_id)
109
- case model_family(model_id)
110
- when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o',
111
- 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini' then true
112
- else false
113
- end
114
- end
115
-
116
- def supports_json_mode?(model_id)
117
- supports_structured_output?(model_id)
118
- end
119
-
120
45
  PRICES = {
121
46
  gpt5: { input: 1.25, output: 10.0, cached_input: 0.125 },
122
47
  gpt5_mini: { input: 0.25, output: 2.0, cached_input: 0.025 },
@@ -124,21 +49,19 @@ module RubyLLM
124
49
  gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
125
50
  gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
126
51
  gpt41_nano: { input: 0.1, output: 0.4 },
127
- chatgpt4o: { input: 5.0, output: 15.0 },
128
52
  gpt4: { input: 10.0, output: 30.0 },
129
53
  gpt4_turbo: { input: 10.0, output: 30.0 },
130
- gpt45: { input: 75.0, output: 150.0 },
131
54
  gpt35_turbo: { input: 0.5, output: 1.5 },
132
55
  gpt4o: { input: 2.5, output: 10.0 },
133
- gpt4o_audio: { input: 2.5, output: 10.0, audio_input: 40.0, audio_output: 80.0 },
56
+ gpt4o_audio: { input: 2.5, output: 10.0 },
134
57
  gpt4o_mini: { input: 0.15, output: 0.6 },
135
- gpt4o_mini_audio: { input: 0.15, output: 0.6, audio_input: 10.0, audio_output: 20.0 },
58
+ gpt4o_mini_audio: { input: 0.15, output: 0.6 },
136
59
  gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
137
- gpt4o_mini_transcribe: { input: 1.25, output: 5.0, audio_input: 3.0 },
60
+ gpt4o_mini_transcribe: { input: 1.25, output: 5.0 },
138
61
  gpt4o_mini_tts: { input: 0.6, output: 12.0 },
139
62
  gpt4o_realtime: { input: 5.0, output: 20.0 },
140
63
  gpt4o_search: { input: 2.5, output: 10.0 },
141
- gpt4o_transcribe: { input: 2.5, output: 10.0, audio_input: 6.0 },
64
+ gpt4o_transcribe: { input: 2.5, output: 10.0 },
142
65
  o1: { input: 15.0, output: 60.0 },
143
66
  o1_mini: { input: 1.1, output: 4.4 },
144
67
  o1_pro: { input: 150.0, output: 600.0 },
@@ -154,157 +77,126 @@ module RubyLLM
154
77
  moderation: { price: 0.0 }
155
78
  }.freeze
156
79
 
157
- def model_family(model_id)
158
- MODEL_PATTERNS.each do |family, pattern|
159
- return family.to_s if model_id.match?(pattern)
160
- end
161
- 'other'
162
- end
163
-
164
- def input_price_for(model_id)
165
- family = model_family(model_id).to_sym
166
- prices = PRICES.fetch(family, { input: default_input_price })
167
- prices[:input] || prices[:price] || default_input_price
80
+ def supports_tool_choice?(_model_id)
81
+ true
168
82
  end
169
83
 
170
- def cached_input_price_for(model_id)
171
- family = model_family(model_id).to_sym
172
- prices = PRICES.fetch(family, {})
173
- prices[:cached_input]
84
+ def supports_tool_parallel_control?(_model_id)
85
+ true
174
86
  end
175
87
 
176
- def output_price_for(model_id)
177
- family = model_family(model_id).to_sym
178
- prices = PRICES.fetch(family, { output: default_output_price })
179
- prices[:output] || prices[:price] || default_output_price
88
+ def context_window_for(model_id)
89
+ case model_family(model_id)
90
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
91
+ when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
92
+ 'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime', 'gpt4o_search',
93
+ 'gpt4o_transcribe', 'o1_mini' then 128_000
94
+ when 'gpt4' then 8_192
95
+ when 'gpt4o_mini_transcribe' then 16_000
96
+ when 'o1', 'o1_pro', 'o3_mini' then 200_000
97
+ when 'gpt35_turbo' then 16_385
98
+ when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
99
+ 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
100
+ else 4_096
101
+ end
180
102
  end
181
103
 
182
- def model_type(model_id)
104
+ def max_tokens_for(model_id)
183
105
  case model_family(model_id)
184
- when /embedding/ then 'embedding'
185
- when /^tts|whisper|gpt4o_(?:mini_)?(?:transcribe|tts)$/ then 'audio'
186
- when 'moderation' then 'moderation'
187
- when /dall/ then 'image'
188
- else 'chat'
106
+ when 'gpt5', 'gpt5_mini', 'gpt5_nano' then 400_000
107
+ when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
108
+ when 'gpt4' then 8_192
109
+ when 'gpt35_turbo' then 4_096
110
+ when 'gpt4o_mini_transcribe' then 2_000
111
+ when 'o1', 'o1_pro', 'o3_mini' then 100_000
112
+ when 'o1_mini' then 65_536
113
+ when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
114
+ 'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
115
+ else 16_384
189
116
  end
190
117
  end
191
118
 
192
- def default_input_price
193
- 0.50
119
+ def critical_capabilities_for(model_id)
120
+ capabilities = []
121
+ capabilities << 'function_calling' if supports_functions?(model_id)
122
+ capabilities << 'structured_output' if supports_structured_output?(model_id)
123
+ capabilities << 'vision' if supports_vision?(model_id)
124
+ capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
125
+ capabilities
194
126
  end
195
127
 
196
- def default_output_price
197
- 1.50
198
- end
128
+ def pricing_for(model_id)
129
+ standard_pricing = {
130
+ input_per_million: input_price_for(model_id),
131
+ output_per_million: output_price_for(model_id)
132
+ }
199
133
 
200
- def format_display_name(model_id)
201
- model_id.then { |id| humanize(id) }
202
- .then { |name| apply_special_formatting(name) }
203
- end
134
+ cached_price = cached_input_price_for(model_id)
135
+ standard_pricing[:cached_input_per_million] = cached_price if cached_price
204
136
 
205
- def humanize(id)
206
- id.tr('-', ' ')
207
- .split
208
- .map(&:capitalize)
209
- .join(' ')
137
+ { text_tokens: { standard: standard_pricing } }
210
138
  end
211
139
 
212
- def apply_special_formatting(name)
213
- name
214
- .gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
215
- .gsub(/^(?:Gpt|Chatgpt|Tts|Dall E) /) { |m| special_prefix_format(m.strip) }
216
- .gsub(/^O([13]) /, 'O\1-')
217
- .gsub(/^O[13] Mini/, '\0'.tr(' ', '-'))
218
- .gsub(/\d\.\d /, '\0'.sub(' ', '-'))
219
- .gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
220
- .gsub(/\bHd\b/, 'HD')
221
- .gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
222
- .gsub('Text Embedding', 'text-embedding-')
140
+ def model_family(model_id)
141
+ MODEL_PATTERNS.each do |family, pattern|
142
+ return family.to_s if model_id.match?(pattern)
143
+ end
144
+
145
+ 'other'
223
146
  end
224
147
 
225
- def special_prefix_format(prefix)
226
- case prefix # rubocop:disable Style/HashLikeCase
227
- when 'Gpt' then 'GPT-'
228
- when 'Chatgpt' then 'ChatGPT-'
229
- when 'Tts' then 'TTS-'
230
- when 'Dall E' then 'DALL-E-'
148
+ def supports_vision?(model_id)
149
+ case model_family(model_id)
150
+ when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4',
151
+ 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'moderation', 'gpt4o_search'
152
+ true
153
+ else
154
+ false
231
155
  end
232
156
  end
233
157
 
234
- def self.normalize_temperature(temperature, model_id)
235
- if model_id.match?(/^(o\d|gpt-5)/) && !temperature.nil? && !temperature_close_to_one?(temperature)
236
- RubyLLM.logger.debug { "Model #{model_id} requires temperature=1.0, setting that instead." }
237
- 1.0
238
- elsif model_id.match?(/-search/)
239
- RubyLLM.logger.debug { "Model #{model_id} does not accept temperature parameter, removing" }
240
- nil
158
+ def supports_functions?(model_id)
159
+ case model_family(model_id)
160
+ when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4',
161
+ 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
162
+ true
241
163
  else
242
- temperature
164
+ false
243
165
  end
244
166
  end
245
167
 
246
- def self.temperature_close_to_one?(temperature)
247
- (temperature.to_f - 1.0).abs <= Float::EPSILON
168
+ def supports_structured_output?(model_id)
169
+ case model_family(model_id)
170
+ when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4o',
171
+ 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
172
+ true
173
+ else
174
+ false
175
+ end
248
176
  end
249
177
 
250
- def modalities_for(model_id)
251
- modalities = {
252
- input: ['text'],
253
- output: ['text']
254
- }
255
-
256
- # Vision support
257
- modalities[:input] << 'image' if supports_vision?(model_id)
258
- modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
259
- modalities[:input] << 'pdf' if supports_vision?(model_id)
260
- modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
261
- modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
262
- modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
263
- modalities[:output] << 'moderation' if model_id.match?(/moderation/)
264
-
265
- modalities
178
+ def input_price_for(model_id)
179
+ price_for(model_id, :input, 0.50)
266
180
  end
267
181
 
268
- def capabilities_for(model_id) # rubocop:disable Metrics/PerceivedComplexity
269
- capabilities = []
270
-
271
- capabilities << 'streaming' unless model_id.match?(/moderation|embedding/)
272
- capabilities << 'function_calling' if supports_functions?(model_id)
273
- capabilities << 'structured_output' if supports_json_mode?(model_id)
274
- capabilities << 'batch' if model_id.match?(/embedding|batch/)
275
- capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
276
-
277
- if model_id.match?(/gpt-4-turbo|gpt-4o/)
278
- capabilities << 'image_generation' if model_id.match?(/vision/)
279
- capabilities << 'speech_generation' if model_id.match?(/audio/)
280
- capabilities << 'transcription' if model_id.match?(/audio/)
281
- end
282
-
283
- capabilities
182
+ def output_price_for(model_id)
183
+ price_for(model_id, :output, 1.50)
284
184
  end
285
185
 
286
- def pricing_for(model_id)
287
- standard_pricing = {
288
- input_per_million: input_price_for(model_id),
289
- output_per_million: output_price_for(model_id)
290
- }
291
-
292
- if respond_to?(:cached_input_price_for)
293
- cached_price = cached_input_price_for(model_id)
294
- standard_pricing[:cached_input_per_million] = cached_price if cached_price
295
- end
296
-
297
- pricing = { text_tokens: { standard: standard_pricing } }
298
-
299
- if model_id.match?(/embedding|batch/)
300
- pricing[:text_tokens][:batch] = {
301
- input_per_million: standard_pricing[:input_per_million] * 0.5,
302
- output_per_million: standard_pricing[:output_per_million] * 0.5
303
- }
304
- end
186
+ def cached_input_price_for(model_id)
187
+ family = model_family(model_id).to_sym
188
+ PRICES.fetch(family, {})[:cached_input]
189
+ end
305
190
 
306
- pricing
191
+ def price_for(model_id, key, fallback)
192
+ family = model_family(model_id).to_sym
193
+ prices = PRICES.fetch(family, { key => fallback })
194
+ prices[key] || prices[:price] || fallback
307
195
  end
196
+
197
+ module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
198
+ :model_family, :supports_vision?, :supports_functions?, :supports_structured_output?,
199
+ :input_price_for, :output_price_for, :cached_input_price_for, :price_for
308
200
  end
309
201
  end
310
202
  end
@@ -17,14 +17,12 @@ module RubyLLM
17
17
 
18
18
  Model::Info.new(
19
19
  id: model_id,
20
- name: capabilities.format_display_name(model_id),
20
+ name: model_id,
21
21
  provider: slug,
22
- family: capabilities.model_family(model_id),
23
22
  created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
24
23
  context_window: capabilities.context_window_for(model_id),
25
24
  max_output_tokens: capabilities.max_tokens_for(model_id),
26
- modalities: capabilities.modalities_for(model_id),
27
- capabilities: capabilities.capabilities_for(model_id),
25
+ capabilities: capabilities.critical_capabilities_for(model_id),
28
26
  pricing: capabilities.pricing_for(model_id),
29
27
  metadata: {
30
28
  object: model_data['object'],
@@ -35,6 +35,16 @@ module RubyLLM
35
35
  OpenAI::Capabilities
36
36
  end
37
37
 
38
+ def configuration_options
39
+ %i[
40
+ openai_api_key
41
+ openai_api_base
42
+ openai_organization_id
43
+ openai_project_id
44
+ openai_use_system_role
45
+ ]
46
+ end
47
+
38
48
  def configuration_requirements
39
49
  %i[openai_api_key]
40
50
  end
@@ -14,7 +14,7 @@ module RubyLLM
14
14
  end
15
15
 
16
16
  def render_image_payload(prompt, model:, size:)
17
- RubyLLM.logger.debug "Ignoring size #{size}. OpenRouter image generation does not support size parameter."
17
+ RubyLLM.logger.debug { "Ignoring size #{size}. OpenRouter image generation does not support size parameter." }
18
18
  {
19
19
  model: model,
20
20
  messages: [
@@ -49,6 +49,10 @@ module RubyLLM
49
49
  end
50
50
 
51
51
  class << self
52
+ def configuration_options
53
+ %i[openrouter_api_key openrouter_api_base]
54
+ end
55
+
52
56
  def configuration_requirements
53
57
  %i[openrouter_api_key]
54
58
  end
@@ -3,63 +3,55 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Perplexity
6
- # Determines capabilities and pricing for Perplexity models
6
+ # Provider-level capability checks and narrow registry fallbacks.
7
7
  module Capabilities
8
8
  module_function
9
9
 
10
- def context_window_for(model_id)
11
- case model_id
12
- when /sonar-pro/ then 200_000
13
- else 128_000
14
- end
15
- end
10
+ PRICES = {
11
+ sonar: { input: 1.0, output: 1.0 },
12
+ sonar_pro: { input: 3.0, output: 15.0 },
13
+ sonar_reasoning: { input: 1.0, output: 5.0 },
14
+ sonar_reasoning_pro: { input: 2.0, output: 8.0 },
15
+ sonar_deep_research: {
16
+ input: 2.0,
17
+ output: 8.0,
18
+ reasoning_output: 3.0
19
+ }
20
+ }.freeze
16
21
 
17
- def max_tokens_for(model_id)
18
- case model_id
19
- when /sonar-(?:pro|reasoning-pro)/ then 8_192
20
- else 4_096
21
- end
22
+ def supports_tool_choice?(_model_id)
23
+ false
22
24
  end
23
25
 
24
- def input_price_for(model_id)
25
- PRICES.dig(model_family(model_id), :input) || 1.0
26
+ def supports_tool_parallel_control?(_model_id)
27
+ false
26
28
  end
27
29
 
28
- def output_price_for(model_id)
29
- PRICES.dig(model_family(model_id), :output) || 1.0
30
+ def context_window_for(model_id)
31
+ model_id.match?(/sonar-pro/) ? 200_000 : 128_000
30
32
  end
31
33
 
32
- def supports_vision?(model_id)
33
- case model_id
34
- when /sonar-reasoning-pro/, /sonar-reasoning/, /sonar-pro/, /sonar/ then true
35
- else false
36
- end
34
+ def max_tokens_for(model_id)
35
+ model_id.match?(/sonar-(?:pro|reasoning-pro)/) ? 8_192 : 4_096
37
36
  end
38
37
 
39
- def supports_functions?(_model_id)
40
- false
38
+ def critical_capabilities_for(model_id)
39
+ capabilities = []
40
+ capabilities << 'vision' if model_id.match?(/sonar(?:-pro|-reasoning(?:-pro)?)?$/)
41
+ capabilities << 'reasoning' if model_id.match?(/reasoning|deep-research/)
42
+ capabilities
41
43
  end
42
44
 
43
- def supports_json_mode?(_model_id)
44
- true
45
- end
45
+ def pricing_for(model_id)
46
+ prices = PRICES.fetch(model_family(model_id), { input: 1.0, output: 1.0 })
46
47
 
47
- def format_display_name(model_id)
48
- case model_id
49
- when 'sonar' then 'Sonar'
50
- when 'sonar-pro' then 'Sonar Pro'
51
- when 'sonar-reasoning' then 'Sonar Reasoning'
52
- when 'sonar-reasoning-pro' then 'Sonar Reasoning Pro'
53
- when 'sonar-deep-research' then 'Sonar Deep Research'
54
- else
55
- model_id.split('-')
56
- .map(&:capitalize)
57
- .join(' ')
58
- end
59
- end
48
+ standard = {
49
+ input_per_million: prices[:input],
50
+ output_per_million: prices[:output]
51
+ }
52
+ standard[:reasoning_output_per_million] = prices[:reasoning_output] if prices[:reasoning_output]
60
53
 
61
- def model_type(_model_id)
62
- 'chat'
54
+ { text_tokens: { standard: standard } }
63
55
  end
64
56
 
65
57
  def model_family(model_id)
@@ -73,64 +65,7 @@ module RubyLLM
73
65
  end
74
66
  end
75
67
 
76
- def modalities_for(_model_id)
77
- {
78
- input: ['text'],
79
- output: ['text']
80
- }
81
- end
82
-
83
- def capabilities_for(model_id)
84
- capabilities = %w[streaming json_mode]
85
- capabilities << 'vision' if supports_vision?(model_id)
86
- capabilities
87
- end
88
-
89
- def pricing_for(model_id)
90
- family = model_family(model_id)
91
- prices = PRICES.fetch(family, { input: 1.0, output: 1.0 })
92
-
93
- standard_pricing = {
94
- input_per_million: prices[:input],
95
- output_per_million: prices[:output]
96
- }
97
-
98
- standard_pricing[:citation_per_million] = prices[:citation] if prices[:citation]
99
- standard_pricing[:reasoning_per_million] = prices[:reasoning] if prices[:reasoning]
100
- standard_pricing[:search_per_thousand] = prices[:search_queries] if prices[:search_queries]
101
-
102
- {
103
- text_tokens: {
104
- standard: standard_pricing
105
- }
106
- }
107
- end
108
-
109
- PRICES = {
110
- sonar: {
111
- input: 1.0,
112
- output: 1.0
113
- },
114
- sonar_pro: {
115
- input: 3.0,
116
- output: 15.0
117
- },
118
- sonar_reasoning: {
119
- input: 1.0,
120
- output: 5.0
121
- },
122
- sonar_reasoning_pro: {
123
- input: 2.0,
124
- output: 8.0
125
- },
126
- sonar_deep_research: {
127
- input: 2.0,
128
- output: 8.0,
129
- citation: 2.0,
130
- reasoning: 3.0,
131
- search_queries: 5.0
132
- }
133
- }.freeze
68
+ module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for, :model_family
134
69
  end
135
70
  end
136
71
  end
@@ -5,33 +5,31 @@ module RubyLLM
5
5
  class Perplexity
6
6
  # Models methods of the Perplexity API integration
7
7
  module Models
8
+ MODEL_IDS = %w[
9
+ sonar
10
+ sonar-pro
11
+ sonar-reasoning
12
+ sonar-reasoning-pro
13
+ sonar-deep-research
14
+ ].freeze
15
+
8
16
  def list_models(**)
9
17
  slug = 'perplexity'
10
- capabilities = Perplexity::Capabilities
11
- parse_list_models_response(nil, slug, capabilities)
18
+ parse_list_models_response(nil, slug, Perplexity::Capabilities)
12
19
  end
13
20
 
14
21
  def parse_list_models_response(_response, slug, capabilities)
15
- [
16
- create_model_info('sonar', slug, capabilities),
17
- create_model_info('sonar-pro', slug, capabilities),
18
- create_model_info('sonar-reasoning', slug, capabilities),
19
- create_model_info('sonar-reasoning-pro', slug, capabilities),
20
- create_model_info('sonar-deep-research', slug, capabilities)
21
- ]
22
+ MODEL_IDS.map { |id| create_model_info(id, slug, capabilities) }
22
23
  end
23
24
 
24
25
  def create_model_info(id, slug, capabilities)
25
26
  Model::Info.new(
26
27
  id: id,
27
- name: capabilities.format_display_name(id),
28
+ name: id,
28
29
  provider: slug,
29
- family: capabilities.model_family(id).to_s,
30
- created_at: Time.now,
31
30
  context_window: capabilities.context_window_for(id),
32
31
  max_output_tokens: capabilities.max_tokens_for(id),
33
- modalities: capabilities.modalities_for(id),
34
- capabilities: capabilities.capabilities_for(id),
32
+ capabilities: capabilities.critical_capabilities_for(id),
35
33
  pricing: capabilities.pricing_for(id),
36
34
  metadata: {}
37
35
  )
@@ -23,6 +23,10 @@ module RubyLLM
23
23
  Perplexity::Capabilities
24
24
  end
25
25
 
26
+ def configuration_options
27
+ %i[perplexity_api_key]
28
+ end
29
+
26
30
  def configuration_requirements
27
31
  %i[perplexity_api_key]
28
32
  end
@@ -40,6 +40,10 @@ module RubyLLM
40
40
  end
41
41
 
42
42
  class << self
43
+ def configuration_options
44
+ %i[vertexai_project_id vertexai_location vertexai_service_account_key]
45
+ end
46
+
43
47
  def configuration_requirements
44
48
  %i[vertexai_project_id vertexai_location]
45
49
  end