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.
- checksums.yaml +4 -4
- data/README.md +11 -7
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +33 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +2 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +2 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +13 -6
- data/lib/ruby_llm/active_record/message_methods.rb +17 -0
- data/lib/ruby_llm/active_record/model_methods.rb +1 -1
- data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/ruby_llm/agent.rb +11 -0
- data/lib/ruby_llm/aliases.json +34 -15
- data/lib/ruby_llm/attachment.rb +3 -0
- data/lib/ruby_llm/configuration.rb +54 -73
- data/lib/ruby_llm/connection.rb +1 -3
- data/lib/ruby_llm/error.rb +5 -0
- data/lib/ruby_llm/model/info.rb +14 -12
- data/lib/ruby_llm/models.json +7446 -10126
- data/lib/ruby_llm/models.rb +10 -3
- data/lib/ruby_llm/provider.rb +5 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -8
- data/lib/ruby_llm/providers/anthropic.rb +4 -0
- data/lib/ruby_llm/providers/azure.rb +4 -0
- data/lib/ruby_llm/providers/bedrock.rb +4 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -119
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack.rb +4 -0
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama.rb +4 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +95 -203
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +4 -0
- data/lib/ruby_llm/providers/vertexai.rb +4 -0
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +6 -5
- data/lib/tasks/vcr.rake +1 -1
- metadata +49 -11
- 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
|
-
#
|
|
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
|
|
31
|
-
gpt5_nano: /^gpt-5
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
158
|
-
|
|
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
|
|
171
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
104
|
+
def max_tokens_for(model_id)
|
|
183
105
|
case model_family(model_id)
|
|
184
|
-
when
|
|
185
|
-
when
|
|
186
|
-
when '
|
|
187
|
-
when
|
|
188
|
-
|
|
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
|
|
193
|
-
|
|
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
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
206
|
-
id.tr('-', ' ')
|
|
207
|
-
.split
|
|
208
|
-
.map(&:capitalize)
|
|
209
|
-
.join(' ')
|
|
137
|
+
{ text_tokens: { standard: standard_pricing } }
|
|
210
138
|
end
|
|
211
139
|
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
226
|
-
case
|
|
227
|
-
when '
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
164
|
+
false
|
|
243
165
|
end
|
|
244
166
|
end
|
|
245
167
|
|
|
246
|
-
def
|
|
247
|
-
(
|
|
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
|
|
251
|
-
|
|
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
|
|
269
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
@@ -3,63 +3,55 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class Perplexity
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks and narrow registry fallbacks.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
18
|
-
|
|
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
|
|
25
|
-
|
|
26
|
+
def supports_tool_parallel_control?(_model_id)
|
|
27
|
+
false
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
+
def context_window_for(model_id)
|
|
31
|
+
model_id.match?(/sonar-pro/) ? 200_000 : 128_000
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
def
|
|
33
|
-
|
|
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
|
|
40
|
-
|
|
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
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
)
|