ruby_llm 1.12.0 → 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 -5
- 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 +2 -2
- 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 +37 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -2
- 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 +10 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +87 -20
- data/lib/ruby_llm/active_record/chat_methods.rb +80 -22
- 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 +50 -8
- data/lib/ruby_llm/aliases.json +60 -21
- data/lib/ruby_llm/attachment.rb +4 -1
- data/lib/ruby_llm/chat.rb +113 -12
- data/lib/ruby_llm/configuration.rb +65 -66
- data/lib/ruby_llm/connection.rb +11 -7
- data/lib/ruby_llm/content.rb +6 -2
- data/lib/ruby_llm/error.rb +37 -1
- data/lib/ruby_llm/message.rb +5 -3
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +12279 -13517
- data/lib/ruby_llm/models.rb +16 -6
- data/lib/ruby_llm/provider.rb +10 -1
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +22 -5
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +1 -1
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/azure/models.rb +1 -1
- data/lib/ruby_llm/providers/azure.rb +92 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +50 -5
- data/lib/ruby_llm/providers/bedrock/models.rb +17 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +8 -4
- data/lib/ruby_llm/providers/bedrock.rb +9 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +20 -4
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +19 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +2 -1
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +95 -195
- data/lib/ruby_llm/providers/openai/chat.rb +15 -5
- data/lib/ruby_llm/providers/openai/media.rb +4 -1
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/temperature.rb +2 -2
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +19 -5
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter.rb +35 -1
- 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/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +18 -6
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/stream_accumulator.rb +10 -5
- data/lib/ruby_llm/streaming.rb +7 -7
- data/lib/ruby_llm/tool.rb +48 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/models.rake +33 -7
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +1 -1
- metadata +56 -15
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
|
@@ -15,6 +15,14 @@ module RubyLLM
|
|
|
15
15
|
!model_id.match?(/embed|moderation|ocr|voxtral|transcriptions|mistral-(tiny|small)-(2312|2402)/)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
def supports_tool_choice?(_model_id)
|
|
19
|
+
true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def supports_tool_parallel_control?(_model_id)
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
def supports_vision?(model_id)
|
|
19
27
|
model_id.match?(/pixtral|mistral-small-(2503|2506)|mistral-medium/)
|
|
20
28
|
end
|
|
@@ -23,7 +23,8 @@ module RubyLLM
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# rubocop:disable Metrics/ParameterLists
|
|
26
|
-
def render_payload(messages, tools:, temperature:, model:, stream: false,
|
|
26
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false,
|
|
27
|
+
schema: nil, thinking: nil, tool_prefs: nil)
|
|
27
28
|
payload = super
|
|
28
29
|
payload.delete(:stream_options)
|
|
29
30
|
payload.delete(:reasoning_effort)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Ollama
|
|
6
|
+
# Determines capabilities for Ollama models
|
|
7
|
+
module Capabilities
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def supports_tool_choice?(_model_id)
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def supports_tool_parallel_control?(_model_id)
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -13,10 +13,16 @@ module RubyLLM
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def headers
|
|
16
|
-
{}
|
|
16
|
+
return {} unless @config.ollama_api_key
|
|
17
|
+
|
|
18
|
+
{ 'Authorization' => "Bearer #{@config.ollama_api_key}" }
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
class << self
|
|
22
|
+
def configuration_options
|
|
23
|
+
%i[ollama_api_base ollama_api_key]
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
def configuration_requirements
|
|
21
27
|
%i[ollama_api_base]
|
|
22
28
|
end
|
|
@@ -24,6 +30,10 @@ module RubyLLM
|
|
|
24
30
|
def local?
|
|
25
31
|
true
|
|
26
32
|
end
|
|
33
|
+
|
|
34
|
+
def capabilities
|
|
35
|
+
Ollama::Capabilities
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
end
|
|
29
39
|
end
|
|
@@ -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,71 +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_structured_output?(model_id)
|
|
101
|
-
case model_family(model_id)
|
|
102
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o',
|
|
103
|
-
'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini' then true
|
|
104
|
-
else false
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def supports_json_mode?(model_id)
|
|
109
|
-
supports_structured_output?(model_id)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
45
|
PRICES = {
|
|
113
46
|
gpt5: { input: 1.25, output: 10.0, cached_input: 0.125 },
|
|
114
47
|
gpt5_mini: { input: 0.25, output: 2.0, cached_input: 0.025 },
|
|
@@ -116,21 +49,19 @@ module RubyLLM
|
|
|
116
49
|
gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
|
|
117
50
|
gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
|
|
118
51
|
gpt41_nano: { input: 0.1, output: 0.4 },
|
|
119
|
-
chatgpt4o: { input: 5.0, output: 15.0 },
|
|
120
52
|
gpt4: { input: 10.0, output: 30.0 },
|
|
121
53
|
gpt4_turbo: { input: 10.0, output: 30.0 },
|
|
122
|
-
gpt45: { input: 75.0, output: 150.0 },
|
|
123
54
|
gpt35_turbo: { input: 0.5, output: 1.5 },
|
|
124
55
|
gpt4o: { input: 2.5, output: 10.0 },
|
|
125
|
-
gpt4o_audio: { input: 2.5, output: 10.0
|
|
56
|
+
gpt4o_audio: { input: 2.5, output: 10.0 },
|
|
126
57
|
gpt4o_mini: { input: 0.15, output: 0.6 },
|
|
127
|
-
gpt4o_mini_audio: { input: 0.15, output: 0.6
|
|
58
|
+
gpt4o_mini_audio: { input: 0.15, output: 0.6 },
|
|
128
59
|
gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
|
|
129
|
-
gpt4o_mini_transcribe: { input: 1.25, output: 5.0
|
|
60
|
+
gpt4o_mini_transcribe: { input: 1.25, output: 5.0 },
|
|
130
61
|
gpt4o_mini_tts: { input: 0.6, output: 12.0 },
|
|
131
62
|
gpt4o_realtime: { input: 5.0, output: 20.0 },
|
|
132
63
|
gpt4o_search: { input: 2.5, output: 10.0 },
|
|
133
|
-
gpt4o_transcribe: { input: 2.5, output: 10.0
|
|
64
|
+
gpt4o_transcribe: { input: 2.5, output: 10.0 },
|
|
134
65
|
o1: { input: 15.0, output: 60.0 },
|
|
135
66
|
o1_mini: { input: 1.1, output: 4.4 },
|
|
136
67
|
o1_pro: { input: 150.0, output: 600.0 },
|
|
@@ -146,157 +77,126 @@ module RubyLLM
|
|
|
146
77
|
moderation: { price: 0.0 }
|
|
147
78
|
}.freeze
|
|
148
79
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
return family.to_s if model_id.match?(pattern)
|
|
152
|
-
end
|
|
153
|
-
'other'
|
|
80
|
+
def supports_tool_choice?(_model_id)
|
|
81
|
+
true
|
|
154
82
|
end
|
|
155
83
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
prices = PRICES.fetch(family, { input: default_input_price })
|
|
159
|
-
prices[:input] || prices[:price] || default_input_price
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def cached_input_price_for(model_id)
|
|
163
|
-
family = model_family(model_id).to_sym
|
|
164
|
-
prices = PRICES.fetch(family, {})
|
|
165
|
-
prices[:cached_input]
|
|
84
|
+
def supports_tool_parallel_control?(_model_id)
|
|
85
|
+
true
|
|
166
86
|
end
|
|
167
87
|
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
172
102
|
end
|
|
173
103
|
|
|
174
|
-
def
|
|
104
|
+
def max_tokens_for(model_id)
|
|
175
105
|
case model_family(model_id)
|
|
176
|
-
when
|
|
177
|
-
when
|
|
178
|
-
when '
|
|
179
|
-
when
|
|
180
|
-
|
|
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
|
|
181
116
|
end
|
|
182
117
|
end
|
|
183
118
|
|
|
184
|
-
def
|
|
185
|
-
|
|
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
|
|
186
126
|
end
|
|
187
127
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
}
|
|
191
133
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
.then { |name| apply_special_formatting(name) }
|
|
195
|
-
end
|
|
134
|
+
cached_price = cached_input_price_for(model_id)
|
|
135
|
+
standard_pricing[:cached_input_per_million] = cached_price if cached_price
|
|
196
136
|
|
|
197
|
-
|
|
198
|
-
id.tr('-', ' ')
|
|
199
|
-
.split
|
|
200
|
-
.map(&:capitalize)
|
|
201
|
-
.join(' ')
|
|
137
|
+
{ text_tokens: { standard: standard_pricing } }
|
|
202
138
|
end
|
|
203
139
|
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
.gsub(/\d\.\d /, '\0'.sub(' ', '-'))
|
|
211
|
-
.gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
|
|
212
|
-
.gsub(/\bHd\b/, 'HD')
|
|
213
|
-
.gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
|
|
214
|
-
.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'
|
|
215
146
|
end
|
|
216
147
|
|
|
217
|
-
def
|
|
218
|
-
case
|
|
219
|
-
when '
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
223
155
|
end
|
|
224
156
|
end
|
|
225
157
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
RubyLLM.logger.debug "Model #{model_id} does not accept temperature parameter, removing"
|
|
232
|
-
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
|
|
233
163
|
else
|
|
234
|
-
|
|
164
|
+
false
|
|
235
165
|
end
|
|
236
166
|
end
|
|
237
167
|
|
|
238
|
-
def
|
|
239
|
-
(
|
|
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
|
|
240
176
|
end
|
|
241
177
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
input: ['text'],
|
|
245
|
-
output: ['text']
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
# Vision support
|
|
249
|
-
modalities[:input] << 'image' if supports_vision?(model_id)
|
|
250
|
-
modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
|
|
251
|
-
modalities[:input] << 'pdf' if supports_vision?(model_id)
|
|
252
|
-
modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
|
|
253
|
-
modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
|
|
254
|
-
modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
|
|
255
|
-
modalities[:output] << 'moderation' if model_id.match?(/moderation/)
|
|
256
|
-
|
|
257
|
-
modalities
|
|
178
|
+
def input_price_for(model_id)
|
|
179
|
+
price_for(model_id, :input, 0.50)
|
|
258
180
|
end
|
|
259
181
|
|
|
260
|
-
def
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
capabilities << 'streaming' unless model_id.match?(/moderation|embedding/)
|
|
264
|
-
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
265
|
-
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
|
266
|
-
capabilities << 'batch' if model_id.match?(/embedding|batch/)
|
|
267
|
-
capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
|
|
268
|
-
|
|
269
|
-
if model_id.match?(/gpt-4-turbo|gpt-4o/)
|
|
270
|
-
capabilities << 'image_generation' if model_id.match?(/vision/)
|
|
271
|
-
capabilities << 'speech_generation' if model_id.match?(/audio/)
|
|
272
|
-
capabilities << 'transcription' if model_id.match?(/audio/)
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
capabilities
|
|
182
|
+
def output_price_for(model_id)
|
|
183
|
+
price_for(model_id, :output, 1.50)
|
|
276
184
|
end
|
|
277
185
|
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if respond_to?(:cached_input_price_for)
|
|
285
|
-
cached_price = cached_input_price_for(model_id)
|
|
286
|
-
standard_pricing[:cached_input_per_million] = cached_price if cached_price
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
pricing = { text_tokens: { standard: standard_pricing } }
|
|
290
|
-
|
|
291
|
-
if model_id.match?(/embedding|batch/)
|
|
292
|
-
pricing[:text_tokens][:batch] = {
|
|
293
|
-
input_per_million: standard_pricing[:input_per_million] * 0.5,
|
|
294
|
-
output_per_million: standard_pricing[:output_per_million] * 0.5
|
|
295
|
-
}
|
|
296
|
-
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
|
|
297
190
|
|
|
298
|
-
|
|
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
|
|
299
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
|
|
300
200
|
end
|
|
301
201
|
end
|
|
302
202
|
end
|
|
@@ -11,7 +11,10 @@ module RubyLLM
|
|
|
11
11
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists,Metrics/PerceivedComplexity
|
|
15
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil,
|
|
16
|
+
thinking: nil, tool_prefs: nil)
|
|
17
|
+
tool_prefs ||= {}
|
|
15
18
|
payload = {
|
|
16
19
|
model: model.id,
|
|
17
20
|
messages: format_messages(messages),
|
|
@@ -19,16 +22,22 @@ module RubyLLM
|
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
payload[:temperature] = temperature unless temperature.nil?
|
|
22
|
-
|
|
25
|
+
if tools.any?
|
|
26
|
+
payload[:tools] = tools.map { |_, tool| tool_for(tool) }
|
|
27
|
+
payload[:tool_choice] = build_tool_choice(tool_prefs[:choice]) unless tool_prefs[:choice].nil?
|
|
28
|
+
payload[:parallel_tool_calls] = tool_prefs[:calls] == :many unless tool_prefs[:calls].nil?
|
|
29
|
+
end
|
|
23
30
|
|
|
24
31
|
if schema
|
|
25
|
-
|
|
32
|
+
schema_name = schema[:name]
|
|
33
|
+
schema_def = schema[:schema]
|
|
34
|
+
strict = schema[:strict]
|
|
26
35
|
|
|
27
36
|
payload[:response_format] = {
|
|
28
37
|
type: 'json_schema',
|
|
29
38
|
json_schema: {
|
|
30
|
-
name:
|
|
31
|
-
schema:
|
|
39
|
+
name: schema_name,
|
|
40
|
+
schema: schema_def,
|
|
32
41
|
strict: strict
|
|
33
42
|
}
|
|
34
43
|
}
|
|
@@ -40,6 +49,7 @@ module RubyLLM
|
|
|
40
49
|
payload[:stream_options] = { include_usage: true } if stream
|
|
41
50
|
payload
|
|
42
51
|
end
|
|
52
|
+
# rubocop:enable Metrics/ParameterLists,Metrics/PerceivedComplexity
|
|
43
53
|
|
|
44
54
|
def parse_completion_response(response)
|
|
45
55
|
data = response.body
|
|
@@ -8,7 +8,10 @@ module RubyLLM
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
|
|
11
|
-
|
|
11
|
+
if content.is_a?(RubyLLM::Content::Raw)
|
|
12
|
+
value = content.value
|
|
13
|
+
return value.is_a?(Hash) ? value.to_json : value
|
|
14
|
+
end
|
|
12
15
|
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
|
13
16
|
return content unless content.is_a?(Content)
|
|
14
17
|
|
|
@@ -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'],
|
|
@@ -9,10 +9,10 @@ module RubyLLM
|
|
|
9
9
|
|
|
10
10
|
def normalize(temperature, model_id)
|
|
11
11
|
if model_id.match?(/^(o\d|gpt-5)/) && !temperature.nil? && !temperature_close_to_one?(temperature)
|
|
12
|
-
RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, setting that instead."
|
|
12
|
+
RubyLLM.logger.debug { "Model #{model_id} requires temperature=1.0, setting that instead." }
|
|
13
13
|
1.0
|
|
14
14
|
elsif model_id.include?('-search')
|
|
15
|
-
RubyLLM.logger.debug "Model #{model_id} does not accept temperature parameter, removing"
|
|
15
|
+
RubyLLM.logger.debug { "Model #{model_id} does not accept temperature parameter, removing" }
|
|
16
16
|
nil
|
|
17
17
|
else
|
|
18
18
|
temperature
|
|
@@ -53,7 +53,7 @@ module RubyLLM
|
|
|
53
53
|
return nil unless tool_calls&.any?
|
|
54
54
|
|
|
55
55
|
tool_calls.map do |_, tc|
|
|
56
|
-
{
|
|
56
|
+
call = {
|
|
57
57
|
id: tc.id,
|
|
58
58
|
type: 'function',
|
|
59
59
|
function: {
|
|
@@ -61,6 +61,12 @@ module RubyLLM
|
|
|
61
61
|
arguments: JSON.generate(tc.arguments)
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
if tc.thought_signature
|
|
65
|
+
call[:extra_content] = {
|
|
66
|
+
google: { thought_signature: tc.thought_signature }
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
call
|
|
64
70
|
end
|
|
65
71
|
end
|
|
66
72
|
|
|
@@ -87,11 +93,30 @@ module RubyLLM
|
|
|
87
93
|
parse_tool_call_arguments(tc)
|
|
88
94
|
else
|
|
89
95
|
tc.dig('function', 'arguments')
|
|
90
|
-
end
|
|
96
|
+
end,
|
|
97
|
+
thought_signature: extract_tool_call_thought_signature(tc)
|
|
91
98
|
)
|
|
92
99
|
]
|
|
93
100
|
end
|
|
94
101
|
end
|
|
102
|
+
|
|
103
|
+
def build_tool_choice(tool_choice)
|
|
104
|
+
case tool_choice
|
|
105
|
+
when :auto, :none, :required
|
|
106
|
+
tool_choice
|
|
107
|
+
else
|
|
108
|
+
{
|
|
109
|
+
type: 'function',
|
|
110
|
+
function: {
|
|
111
|
+
name: tool_choice
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def extract_tool_call_thought_signature(tool_call)
|
|
118
|
+
tool_call.dig('extra_content', 'google', 'thought_signature')
|
|
119
|
+
end
|
|
95
120
|
end
|
|
96
121
|
end
|
|
97
122
|
end
|
|
@@ -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
|