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
|
@@ -18,12 +18,15 @@ module RubyLLM
|
|
|
18
18
|
{}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# rubocop:disable Metrics/ParameterLists
|
|
22
|
+
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
|
|
23
|
+
tool_prefs: nil, &)
|
|
22
24
|
normalized_params = normalize_params(params, model:)
|
|
23
25
|
|
|
24
26
|
super(
|
|
25
27
|
messages,
|
|
26
28
|
tools: tools,
|
|
29
|
+
tool_prefs: tool_prefs,
|
|
27
30
|
temperature: temperature,
|
|
28
31
|
model: model,
|
|
29
32
|
params: normalized_params,
|
|
@@ -33,6 +36,7 @@ module RubyLLM
|
|
|
33
36
|
&
|
|
34
37
|
)
|
|
35
38
|
end
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
36
40
|
|
|
37
41
|
def parse_error(response)
|
|
38
42
|
return if response.body.nil? || response.body.empty?
|
|
@@ -49,6 +53,10 @@ module RubyLLM
|
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
class << self
|
|
56
|
+
def configuration_options
|
|
57
|
+
%i[bedrock_api_key bedrock_secret_key bedrock_region bedrock_session_token]
|
|
58
|
+
end
|
|
59
|
+
|
|
52
60
|
def configuration_requirements
|
|
53
61
|
%i[bedrock_api_key bedrock_secret_key bedrock_region]
|
|
54
62
|
end
|
|
@@ -3,127 +3,17 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class DeepSeek
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks used outside the model registry.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
when /deepseek-(?:chat|reasoner)/ then 64_000
|
|
13
|
-
else 32_768
|
|
14
|
-
end
|
|
10
|
+
def supports_tool_choice?(_model_id)
|
|
11
|
+
true
|
|
15
12
|
end
|
|
16
13
|
|
|
17
|
-
def
|
|
18
|
-
case model_id
|
|
19
|
-
when /deepseek-(?:chat|reasoner)/ then 8_192
|
|
20
|
-
else 4_096
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def input_price_for(model_id)
|
|
25
|
-
PRICES.dig(model_family(model_id), :input_miss) || default_input_price
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def output_price_for(model_id)
|
|
29
|
-
PRICES.dig(model_family(model_id), :output) || default_output_price
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def cache_hit_price_for(model_id)
|
|
33
|
-
PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def supports_vision?(_model_id)
|
|
14
|
+
def supports_tool_parallel_control?(_model_id)
|
|
37
15
|
false
|
|
38
16
|
end
|
|
39
|
-
|
|
40
|
-
def supports_functions?(model_id)
|
|
41
|
-
model_id.match?(/deepseek-chat/)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def supports_json_mode?(_model_id)
|
|
45
|
-
false
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def format_display_name(model_id)
|
|
49
|
-
case model_id
|
|
50
|
-
when 'deepseek-chat' then 'DeepSeek V3'
|
|
51
|
-
when 'deepseek-reasoner' then 'DeepSeek R1'
|
|
52
|
-
else
|
|
53
|
-
model_id.split('-')
|
|
54
|
-
.map(&:capitalize)
|
|
55
|
-
.join(' ')
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def model_type(_model_id)
|
|
60
|
-
'chat'
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def model_family(model_id)
|
|
64
|
-
case model_id
|
|
65
|
-
when /deepseek-reasoner/ then :reasoner
|
|
66
|
-
else :chat
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
PRICES = {
|
|
71
|
-
chat: {
|
|
72
|
-
input_hit: 0.07,
|
|
73
|
-
input_miss: 0.27,
|
|
74
|
-
output: 1.10
|
|
75
|
-
},
|
|
76
|
-
reasoner: {
|
|
77
|
-
input_hit: 0.14,
|
|
78
|
-
input_miss: 0.55,
|
|
79
|
-
output: 2.19
|
|
80
|
-
}
|
|
81
|
-
}.freeze
|
|
82
|
-
|
|
83
|
-
def default_input_price
|
|
84
|
-
0.27
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def default_output_price
|
|
88
|
-
1.10
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def default_cache_hit_price
|
|
92
|
-
0.07
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def modalities_for(_model_id)
|
|
96
|
-
{
|
|
97
|
-
input: ['text'],
|
|
98
|
-
output: ['text']
|
|
99
|
-
}
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def capabilities_for(model_id)
|
|
103
|
-
capabilities = ['streaming']
|
|
104
|
-
|
|
105
|
-
capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
|
|
106
|
-
|
|
107
|
-
capabilities
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def pricing_for(model_id)
|
|
111
|
-
family = model_family(model_id)
|
|
112
|
-
prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
|
|
113
|
-
|
|
114
|
-
standard_pricing = {
|
|
115
|
-
input_per_million: prices[:input_miss],
|
|
116
|
-
output_per_million: prices[:output]
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
|
120
|
-
|
|
121
|
-
{
|
|
122
|
-
text_tokens: {
|
|
123
|
-
standard: standard_pricing
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
end
|
|
127
17
|
end
|
|
128
18
|
end
|
|
129
19
|
end
|
|
@@ -7,7 +7,7 @@ module RubyLLM
|
|
|
7
7
|
include DeepSeek::Chat
|
|
8
8
|
|
|
9
9
|
def api_base
|
|
10
|
-
'https://api.deepseek.com'
|
|
10
|
+
@config.deepseek_api_base || 'https://api.deepseek.com'
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def headers
|
|
@@ -21,6 +21,10 @@ module RubyLLM
|
|
|
21
21
|
DeepSeek::Capabilities
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def configuration_options
|
|
25
|
+
%i[deepseek_api_key deepseek_api_base]
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
def configuration_requirements
|
|
25
29
|
%i[deepseek_api_key]
|
|
26
30
|
end
|
|
@@ -3,13 +3,35 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class Gemini
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks and narrow registry fallbacks.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
PRICES = {
|
|
11
|
+
flash_2: { input: 0.10, output: 0.40 }, # rubocop:disable Naming/VariableNumber
|
|
12
|
+
flash_lite_2: { input: 0.075, output: 0.30 }, # rubocop:disable Naming/VariableNumber
|
|
13
|
+
flash: { input: 0.075, output: 0.30 },
|
|
14
|
+
flash_8b: { input: 0.0375, output: 0.15 },
|
|
15
|
+
pro: { input: 1.25, output: 5.0 },
|
|
16
|
+
pro_2_5: { input: 0.12, output: 0.50 }, # rubocop:disable Naming/VariableNumber
|
|
17
|
+
gemini_embedding: { input: 0.002, output: 0.004 },
|
|
18
|
+
embedding: { input: 0.00, output: 0.00 },
|
|
19
|
+
imagen: { price: 0.03 },
|
|
20
|
+
aqa: { input: 0.00, output: 0.00 }
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def supports_tool_choice?(_model_id)
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def supports_tool_parallel_control?(_model_id)
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
10
31
|
def context_window_for(model_id)
|
|
11
32
|
case model_id
|
|
12
|
-
when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
|
|
33
|
+
when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
|
|
34
|
+
/gemini-1\.5-flash-8b/
|
|
13
35
|
1_048_576
|
|
14
36
|
when /gemini-1\.5-pro/ then 2_097_152
|
|
15
37
|
when /gemini-embedding-exp/ then 8_192
|
|
@@ -23,7 +45,8 @@ module RubyLLM
|
|
|
23
45
|
def max_tokens_for(model_id)
|
|
24
46
|
case model_id
|
|
25
47
|
when /gemini-2\.5-pro-exp-03-25/ then 64_000
|
|
26
|
-
when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
|
|
48
|
+
when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
|
|
49
|
+
/gemini-1\.5-pro/
|
|
27
50
|
8_192
|
|
28
51
|
when /gemini-embedding-exp/ then nil
|
|
29
52
|
when /text-embedding-004/, /embedding-001/ then 768
|
|
@@ -32,18 +55,24 @@ module RubyLLM
|
|
|
32
55
|
end
|
|
33
56
|
end
|
|
34
57
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
def critical_capabilities_for(model_id)
|
|
59
|
+
capabilities = []
|
|
60
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
61
|
+
capabilities << 'structured_output' if supports_structured_output?(model_id)
|
|
62
|
+
capabilities << 'vision' if supports_vision?(model_id)
|
|
63
|
+
capabilities
|
|
40
64
|
end
|
|
41
65
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
def pricing_for(model_id)
|
|
67
|
+
prices = PRICES.fetch(pricing_family(model_id), { input: 0.075, output: 0.30 })
|
|
68
|
+
{
|
|
69
|
+
text_tokens: {
|
|
70
|
+
standard: {
|
|
71
|
+
input_per_million: prices[:input] || prices[:price] || 0.075,
|
|
72
|
+
output_per_million: prices[:output] || prices[:price] || 0.30
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
47
76
|
end
|
|
48
77
|
|
|
49
78
|
def supports_vision?(model_id)
|
|
@@ -52,17 +81,13 @@ module RubyLLM
|
|
|
52
81
|
model_id.match?(/gemini|flash|pro|imagen/)
|
|
53
82
|
end
|
|
54
83
|
|
|
55
|
-
def supports_video?(model_id)
|
|
56
|
-
model_id.match?(/gemini/)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
84
|
def supports_functions?(model_id)
|
|
60
85
|
return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
|
|
61
86
|
|
|
62
87
|
model_id.match?(/gemini|pro|flash/)
|
|
63
88
|
end
|
|
64
89
|
|
|
65
|
-
def
|
|
90
|
+
def supports_structured_output?(model_id)
|
|
66
91
|
if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
|
|
67
92
|
return false
|
|
68
93
|
end
|
|
@@ -70,59 +95,6 @@ module RubyLLM
|
|
|
70
95
|
model_id.match?(/gemini|pro|flash/)
|
|
71
96
|
end
|
|
72
97
|
|
|
73
|
-
def format_display_name(model_id)
|
|
74
|
-
model_id
|
|
75
|
-
.delete_prefix('models/')
|
|
76
|
-
.split('-')
|
|
77
|
-
.map(&:capitalize)
|
|
78
|
-
.join(' ')
|
|
79
|
-
.gsub(/(\d+\.\d+)/, ' \1')
|
|
80
|
-
.gsub(/\s+/, ' ')
|
|
81
|
-
.gsub('Aqa', 'AQA')
|
|
82
|
-
.strip
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def supports_caching?(model_id)
|
|
86
|
-
if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
|
|
87
|
-
return false
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
model_id.match?(/gemini|pro|flash/)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def supports_tuning?(model_id)
|
|
94
|
-
model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def supports_audio?(model_id)
|
|
98
|
-
model_id.match?(/gemini|pro|flash/)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def model_type(model_id)
|
|
102
|
-
case model_id
|
|
103
|
-
when /text-embedding|embedding|gemini-embedding/ then 'embedding'
|
|
104
|
-
when /imagen/ then 'image'
|
|
105
|
-
else 'chat'
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def model_family(model_id)
|
|
110
|
-
case model_id
|
|
111
|
-
when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
|
|
112
|
-
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
|
|
113
|
-
when /gemini-2\.0-flash/ then 'gemini20_flash'
|
|
114
|
-
when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
|
|
115
|
-
when /gemini-1\.5-flash/ then 'gemini15_flash'
|
|
116
|
-
when /gemini-1\.5-pro/ then 'gemini15_pro'
|
|
117
|
-
when /gemini-embedding-exp/ then 'gemini_embedding_exp'
|
|
118
|
-
when /text-embedding-004/ then 'embedding4'
|
|
119
|
-
when /embedding-001/ then 'embedding1'
|
|
120
|
-
when /aqa/ then 'aqa'
|
|
121
|
-
when /imagen-3/ then 'imagen3'
|
|
122
|
-
else 'other'
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
98
|
def pricing_family(model_id)
|
|
127
99
|
case model_id
|
|
128
100
|
when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
|
|
@@ -139,142 +111,8 @@ module RubyLLM
|
|
|
139
111
|
end
|
|
140
112
|
end
|
|
141
113
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def context_length(model_id)
|
|
147
|
-
context_window_for(model_id)
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
PRICES = {
|
|
151
|
-
flash_2: { # rubocop:disable Naming/VariableNumber
|
|
152
|
-
input: 0.10,
|
|
153
|
-
output: 0.40,
|
|
154
|
-
audio_input: 0.70,
|
|
155
|
-
cache: 0.025,
|
|
156
|
-
cache_storage: 1.00,
|
|
157
|
-
grounding_search: 35.00
|
|
158
|
-
},
|
|
159
|
-
flash_lite_2: { # rubocop:disable Naming/VariableNumber
|
|
160
|
-
input: 0.075,
|
|
161
|
-
output: 0.30
|
|
162
|
-
},
|
|
163
|
-
flash: {
|
|
164
|
-
input: 0.075,
|
|
165
|
-
output: 0.30,
|
|
166
|
-
cache: 0.01875,
|
|
167
|
-
cache_storage: 1.00,
|
|
168
|
-
grounding_search: 35.00
|
|
169
|
-
},
|
|
170
|
-
flash_8b: {
|
|
171
|
-
input: 0.0375,
|
|
172
|
-
output: 0.15,
|
|
173
|
-
cache: 0.01,
|
|
174
|
-
cache_storage: 0.25,
|
|
175
|
-
grounding_search: 35.00
|
|
176
|
-
},
|
|
177
|
-
pro: {
|
|
178
|
-
input: 1.25,
|
|
179
|
-
output: 5.0,
|
|
180
|
-
cache: 0.3125,
|
|
181
|
-
cache_storage: 4.50,
|
|
182
|
-
grounding_search: 35.00
|
|
183
|
-
},
|
|
184
|
-
pro_2_5: { # rubocop:disable Naming/VariableNumber
|
|
185
|
-
input: 0.12,
|
|
186
|
-
output: 0.50
|
|
187
|
-
},
|
|
188
|
-
gemini_embedding: {
|
|
189
|
-
input: 0.002,
|
|
190
|
-
output: 0.004
|
|
191
|
-
},
|
|
192
|
-
embedding: {
|
|
193
|
-
input: 0.00,
|
|
194
|
-
output: 0.00
|
|
195
|
-
},
|
|
196
|
-
imagen: {
|
|
197
|
-
price: 0.03
|
|
198
|
-
},
|
|
199
|
-
aqa: {
|
|
200
|
-
input: 0.00,
|
|
201
|
-
output: 0.00
|
|
202
|
-
}
|
|
203
|
-
}.freeze
|
|
204
|
-
|
|
205
|
-
def default_input_price
|
|
206
|
-
0.075
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
def default_output_price
|
|
210
|
-
0.30
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def modalities_for(model_id)
|
|
214
|
-
modalities = {
|
|
215
|
-
input: ['text'],
|
|
216
|
-
output: ['text']
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if supports_vision?(model_id)
|
|
220
|
-
modalities[:input] << 'image'
|
|
221
|
-
modalities[:input] << 'pdf'
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
modalities[:input] << 'video' if supports_video?(model_id)
|
|
225
|
-
modalities[:input] << 'audio' if model_id.match?(/audio/)
|
|
226
|
-
modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
|
|
227
|
-
modalities[:output] = ['image'] if model_id.match?(/imagen/)
|
|
228
|
-
|
|
229
|
-
modalities
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
def capabilities_for(model_id)
|
|
233
|
-
capabilities = ['streaming']
|
|
234
|
-
|
|
235
|
-
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
236
|
-
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
|
237
|
-
capabilities << 'batch' if model_id.match?(/embedding|flash/)
|
|
238
|
-
capabilities << 'caching' if supports_caching?(model_id)
|
|
239
|
-
capabilities << 'fine_tuning' if supports_tuning?(model_id)
|
|
240
|
-
capabilities
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def pricing_for(model_id)
|
|
244
|
-
family = pricing_family(model_id)
|
|
245
|
-
prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
|
|
246
|
-
|
|
247
|
-
standard_pricing = {
|
|
248
|
-
input_per_million: prices[:input],
|
|
249
|
-
output_per_million: prices[:output]
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
|
253
|
-
|
|
254
|
-
batch_pricing = {
|
|
255
|
-
input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
|
|
256
|
-
output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if standard_pricing[:cached_input_per_million]
|
|
260
|
-
batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
pricing = {
|
|
264
|
-
text_tokens: {
|
|
265
|
-
standard: standard_pricing,
|
|
266
|
-
batch: batch_pricing
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if model_id.match?(/embedding|gemini-embedding/)
|
|
271
|
-
pricing[:embeddings] = {
|
|
272
|
-
standard: { input_per_million: prices[:price] || 0.002 }
|
|
273
|
-
}
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
pricing
|
|
277
|
-
end
|
|
114
|
+
module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
|
|
115
|
+
:supports_vision?, :supports_functions?, :supports_structured_output?, :pricing_family
|
|
278
116
|
end
|
|
279
117
|
end
|
|
280
118
|
end
|
|
@@ -14,7 +14,10 @@ module RubyLLM
|
|
|
14
14
|
"models/#{@model}:generateContent"
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
18
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil,
|
|
19
|
+
thinking: nil, tool_prefs: nil)
|
|
20
|
+
tool_prefs ||= {}
|
|
18
21
|
@model = model.id
|
|
19
22
|
payload = {
|
|
20
23
|
contents: format_messages(messages),
|
|
@@ -26,9 +29,15 @@ module RubyLLM
|
|
|
26
29
|
payload[:generationConfig].merge!(structured_output_config(schema, model)) if schema
|
|
27
30
|
payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking&.enabled?
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
if tools.any?
|
|
33
|
+
payload[:tools] = format_tools(tools)
|
|
34
|
+
# Gemini doesn't support controlling parallel tool calls
|
|
35
|
+
payload[:toolConfig] = build_tool_config(tool_prefs[:choice]) unless tool_prefs[:choice].nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
payload
|
|
31
39
|
end
|
|
40
|
+
# rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
32
41
|
|
|
33
42
|
def build_thinking_config(_model, thinking)
|
|
34
43
|
config = { includeThoughts: true }
|
|
@@ -111,6 +120,7 @@ module RubyLLM
|
|
|
111
120
|
tool_calls: tool_calls,
|
|
112
121
|
input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
|
|
113
122
|
output_tokens: calculate_output_tokens(data),
|
|
123
|
+
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
114
124
|
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
115
125
|
model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0],
|
|
116
126
|
raw: response
|
|
@@ -120,6 +130,9 @@ module RubyLLM
|
|
|
120
130
|
def convert_schema_to_gemini(schema)
|
|
121
131
|
return nil unless schema
|
|
122
132
|
|
|
133
|
+
# Extract inner schema if wrapper format (e.g., from RubyLLM::Schema.to_json_schema)
|
|
134
|
+
schema = schema[:schema] || schema
|
|
135
|
+
|
|
123
136
|
GeminiSchema.new(schema).to_h
|
|
124
137
|
end
|
|
125
138
|
|
|
@@ -132,7 +145,10 @@ module RubyLLM
|
|
|
132
145
|
parts = candidate.dig('content', 'parts')
|
|
133
146
|
return '' unless parts&.any?
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
non_thought_parts = parts.reject { |part| part['thought'] }
|
|
149
|
+
return '' unless non_thought_parts.any?
|
|
150
|
+
|
|
151
|
+
build_response_content(non_thought_parts)
|
|
136
152
|
end
|
|
137
153
|
|
|
138
154
|
def extract_text_parts(parts)
|
|
@@ -176,7 +192,7 @@ module RubyLLM
|
|
|
176
192
|
end
|
|
177
193
|
|
|
178
194
|
def build_json_schema(schema)
|
|
179
|
-
normalized = RubyLLM::Utils.deep_dup(schema)
|
|
195
|
+
normalized = RubyLLM::Utils.deep_dup(schema[:schema])
|
|
180
196
|
normalized.delete(:strict)
|
|
181
197
|
normalized.delete('strict')
|
|
182
198
|
RubyLLM::Utils.deep_stringify_keys(normalized)
|
|
@@ -10,7 +10,7 @@ module RubyLLM
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def render_image_payload(prompt, model:, size:)
|
|
13
|
-
RubyLLM.logger.debug "Ignoring size #{size}. Gemini does not support image size customization."
|
|
13
|
+
RubyLLM.logger.debug { "Ignoring size #{size}. Gemini does not support image size customization." }
|
|
14
14
|
@model = model
|
|
15
15
|
{
|
|
16
16
|
instances: [
|
|
@@ -17,14 +17,12 @@ module RubyLLM
|
|
|
17
17
|
|
|
18
18
|
Model::Info.new(
|
|
19
19
|
id: model_id,
|
|
20
|
-
name: model_data['displayName'],
|
|
20
|
+
name: model_data['displayName'] || model_id,
|
|
21
21
|
provider: slug,
|
|
22
|
-
family: capabilities.model_family(model_id),
|
|
23
22
|
created_at: nil,
|
|
24
23
|
context_window: model_data['inputTokenLimit'] || capabilities.context_window_for(model_id),
|
|
25
24
|
max_output_tokens: model_data['outputTokenLimit'] || 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
|
version: model_data['version'],
|
|
@@ -22,6 +22,7 @@ module RubyLLM
|
|
|
22
22
|
),
|
|
23
23
|
input_tokens: extract_input_tokens(data),
|
|
24
24
|
output_tokens: extract_output_tokens(data),
|
|
25
|
+
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
25
26
|
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
26
27
|
tool_calls: extract_tool_calls(data)
|
|
27
28
|
)
|
|
@@ -83,7 +84,7 @@ module RubyLLM
|
|
|
83
84
|
error_data = JSON.parse(data)
|
|
84
85
|
[error_data['error']['code'], error_data['error']['message']]
|
|
85
86
|
rescue JSON::ParserError => e
|
|
86
|
-
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
|
87
|
+
RubyLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
|
|
87
88
|
[500, "Failed to parse error: #{data}"]
|
|
88
89
|
end
|
|
89
90
|
end
|
|
@@ -205,6 +205,25 @@ module RubyLLM
|
|
|
205
205
|
else 'STRING'
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
|
+
|
|
209
|
+
def build_tool_config(tool_choice)
|
|
210
|
+
{
|
|
211
|
+
functionCallingConfig: {
|
|
212
|
+
mode: forced_tool_choice?(tool_choice) ? 'any' : tool_choice
|
|
213
|
+
}.tap do |config|
|
|
214
|
+
# Use allowedFunctionNames to simulate specific tool choice
|
|
215
|
+
config[:allowedFunctionNames] = [tool_choice] if specific_tool_choice?(tool_choice)
|
|
216
|
+
end
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def forced_tool_choice?(tool_choice)
|
|
221
|
+
tool_choice == :required || specific_tool_choice?(tool_choice)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def specific_tool_choice?(tool_choice)
|
|
225
|
+
!%i[auto none required].include?(tool_choice)
|
|
226
|
+
end
|
|
208
227
|
end
|
|
209
228
|
end
|
|
210
229
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class GPUStack
|
|
6
|
+
# Determines capabilities for GPUStack 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
|
|
@@ -21,6 +21,10 @@ module RubyLLM
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
|
+
def configuration_options
|
|
25
|
+
%i[gpustack_api_base gpustack_api_key]
|
|
26
|
+
end
|
|
27
|
+
|
|
24
28
|
def local?
|
|
25
29
|
true
|
|
26
30
|
end
|
|
@@ -28,6 +32,10 @@ module RubyLLM
|
|
|
28
32
|
def configuration_requirements
|
|
29
33
|
%i[gpustack_api_base]
|
|
30
34
|
end
|
|
35
|
+
|
|
36
|
+
def capabilities
|
|
37
|
+
GPUStack::Capabilities
|
|
38
|
+
end
|
|
31
39
|
end
|
|
32
40
|
end
|
|
33
41
|
end
|