dify_llm 1.9.2 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -8
- 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 +4 -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 -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/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- 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 +132 -27
- data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
- data/lib/ruby_llm/active_record/message_methods.rb +58 -8
- 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 +365 -0
- data/lib/ruby_llm/aliases.json +106 -61
- data/lib/ruby_llm/attachment.rb +8 -3
- data/lib/ruby_llm/chat.rb +150 -22
- data/lib/ruby_llm/configuration.rb +65 -65
- 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 +43 -15
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +25039 -12260
- data/lib/ruby_llm/models.rb +185 -24
- data/lib/ruby_llm/provider.rb +26 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
- 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 +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +148 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +104 -65
- data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
- data/lib/ruby_llm/providers/bedrock.rb +69 -52
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/dify/chat.rb +82 -7
- data/lib/ruby_llm/providers/dify/media.rb +2 -2
- data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
- data/lib/ruby_llm/providers/dify.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
- 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 +34 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
- 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/chat.rb +1 -1
- 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 +59 -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/chat.rb +1 -1
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
- data/lib/ruby_llm/providers/openai/chat.rb +101 -7
- data/lib/ruby_llm/providers/openai/media.rb +5 -2
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
- data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +11 -1
- data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
- data/lib/ruby_llm/providers/openrouter.rb +37 -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 +23 -7
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +32 -0
- data/lib/ruby_llm/stream_accumulator.rb +120 -18
- data/lib/ruby_llm/streaming.rb +60 -57
- data/lib/ruby_llm/thinking.rb +49 -0
- data/lib/ruby_llm/tokens.rb +47 -0
- data/lib/ruby_llm/tool.rb +48 -3
- data/lib/ruby_llm/tool_call.rb +6 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +61 -22
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +33 -1
- metadata +67 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
|
@@ -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),
|
|
@@ -24,10 +27,35 @@ module RubyLLM
|
|
|
24
27
|
payload[:generationConfig][:temperature] = temperature unless temperature.nil?
|
|
25
28
|
|
|
26
29
|
payload[:generationConfig].merge!(structured_output_config(schema, model)) if schema
|
|
30
|
+
payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking&.enabled?
|
|
31
|
+
|
|
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
|
|
27
37
|
|
|
28
|
-
payload[:tools] = format_tools(tools) if tools.any?
|
|
29
38
|
payload
|
|
30
39
|
end
|
|
40
|
+
# rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
41
|
+
|
|
42
|
+
def build_thinking_config(_model, thinking)
|
|
43
|
+
config = { includeThoughts: true }
|
|
44
|
+
|
|
45
|
+
config[:thinkingLevel] = resolve_effort_level(thinking) if thinking&.effort
|
|
46
|
+
config[:thinkingBudget] = resolve_budget(thinking) if thinking&.budget
|
|
47
|
+
|
|
48
|
+
config
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resolve_effort_level(thinking)
|
|
52
|
+
thinking.respond_to?(:effort) ? thinking.effort : thinking
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_budget(thinking)
|
|
56
|
+
budget = thinking.respond_to?(:budget) ? thinking.budget : thinking
|
|
57
|
+
budget.is_a?(Integer) ? budget : nil
|
|
58
|
+
end
|
|
31
59
|
|
|
32
60
|
private
|
|
33
61
|
|
|
@@ -56,20 +84,44 @@ module RubyLLM
|
|
|
56
84
|
elsif msg.tool_result?
|
|
57
85
|
format_tool_result(msg)
|
|
58
86
|
else
|
|
59
|
-
|
|
87
|
+
format_message_parts(msg)
|
|
60
88
|
end
|
|
61
89
|
end
|
|
62
90
|
|
|
91
|
+
def format_message_parts(msg)
|
|
92
|
+
parts = []
|
|
93
|
+
|
|
94
|
+
parts << build_thought_part(msg.thinking) if msg.role == :assistant && msg.thinking
|
|
95
|
+
|
|
96
|
+
content_parts = Media.format_content(msg.content)
|
|
97
|
+
parts.concat(content_parts.is_a?(Array) ? content_parts : [content_parts])
|
|
98
|
+
parts
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def build_thought_part(thinking)
|
|
102
|
+
part = { thought: true }
|
|
103
|
+
part[:text] = thinking.text if thinking.text
|
|
104
|
+
part[:thoughtSignature] = thinking.signature if thinking.signature
|
|
105
|
+
part
|
|
106
|
+
end
|
|
107
|
+
|
|
63
108
|
def parse_completion_response(response)
|
|
64
109
|
data = response.body
|
|
110
|
+
parts = data.dig('candidates', 0, 'content', 'parts') || []
|
|
65
111
|
tool_calls = extract_tool_calls(data)
|
|
66
112
|
|
|
67
113
|
Message.new(
|
|
68
114
|
role: :assistant,
|
|
69
|
-
content: parse_content(data),
|
|
115
|
+
content: extract_text_parts(parts) || parse_content(data),
|
|
116
|
+
thinking: Thinking.build(
|
|
117
|
+
text: extract_thought_parts(parts),
|
|
118
|
+
signature: extract_thought_signature(parts)
|
|
119
|
+
),
|
|
70
120
|
tool_calls: tool_calls,
|
|
71
121
|
input_tokens: data.dig('usageMetadata', 'promptTokenCount'),
|
|
72
122
|
output_tokens: calculate_output_tokens(data),
|
|
123
|
+
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
124
|
+
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
73
125
|
model_id: data['modelVersion'] || response.env.url.path.split('/')[3].split(':')[0],
|
|
74
126
|
raw: response
|
|
75
127
|
)
|
|
@@ -78,6 +130,9 @@ module RubyLLM
|
|
|
78
130
|
def convert_schema_to_gemini(schema)
|
|
79
131
|
return nil unless schema
|
|
80
132
|
|
|
133
|
+
# Extract inner schema if wrapper format (e.g., from RubyLLM::Schema.to_json_schema)
|
|
134
|
+
schema = schema[:schema] || schema
|
|
135
|
+
|
|
81
136
|
GeminiSchema.new(schema).to_h
|
|
82
137
|
end
|
|
83
138
|
|
|
@@ -90,7 +145,34 @@ module RubyLLM
|
|
|
90
145
|
parts = candidate.dig('content', 'parts')
|
|
91
146
|
return '' unless parts&.any?
|
|
92
147
|
|
|
93
|
-
|
|
148
|
+
non_thought_parts = parts.reject { |part| part['thought'] }
|
|
149
|
+
return '' unless non_thought_parts.any?
|
|
150
|
+
|
|
151
|
+
build_response_content(non_thought_parts)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def extract_text_parts(parts)
|
|
155
|
+
text_parts = parts.reject { |p| p['thought'] }
|
|
156
|
+
content = text_parts.filter_map { |p| p['text'] }.join
|
|
157
|
+
content.empty? ? nil : content
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_thought_parts(parts)
|
|
161
|
+
thought_parts = parts.select { |p| p['thought'] }
|
|
162
|
+
thoughts = thought_parts.filter_map { |p| p['text'] }.join
|
|
163
|
+
thoughts.empty? ? nil : thoughts
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extract_thought_signature(parts)
|
|
167
|
+
parts.each do |part|
|
|
168
|
+
signature = part['thoughtSignature'] ||
|
|
169
|
+
part['thought_signature'] ||
|
|
170
|
+
part.dig('functionCall', 'thoughtSignature') ||
|
|
171
|
+
part.dig('functionCall', 'thought_signature')
|
|
172
|
+
return signature if signature
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
nil
|
|
94
176
|
end
|
|
95
177
|
|
|
96
178
|
def function_call?(candidate)
|
|
@@ -110,7 +192,7 @@ module RubyLLM
|
|
|
110
192
|
end
|
|
111
193
|
|
|
112
194
|
def build_json_schema(schema)
|
|
113
|
-
normalized = RubyLLM::Utils.deep_dup(schema)
|
|
195
|
+
normalized = RubyLLM::Utils.deep_dup(schema[:schema])
|
|
114
196
|
normalized.delete(:strict)
|
|
115
197
|
normalized.delete('strict')
|
|
116
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'],
|
|
@@ -10,12 +10,20 @@ module RubyLLM
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def build_chunk(data)
|
|
13
|
+
parts = data.dig('candidates', 0, 'content', 'parts') || []
|
|
14
|
+
|
|
13
15
|
Chunk.new(
|
|
14
16
|
role: :assistant,
|
|
15
17
|
model_id: extract_model_id(data),
|
|
16
|
-
content:
|
|
18
|
+
content: extract_text_content(parts),
|
|
19
|
+
thinking: Thinking.build(
|
|
20
|
+
text: extract_thought_content(parts),
|
|
21
|
+
signature: extract_thought_signature(parts)
|
|
22
|
+
),
|
|
17
23
|
input_tokens: extract_input_tokens(data),
|
|
18
24
|
output_tokens: extract_output_tokens(data),
|
|
25
|
+
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
26
|
+
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
19
27
|
tool_calls: extract_tool_calls(data)
|
|
20
28
|
)
|
|
21
29
|
end
|
|
@@ -26,6 +34,30 @@ module RubyLLM
|
|
|
26
34
|
data['modelVersion']
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
def extract_text_content(parts)
|
|
38
|
+
text_parts = parts.reject { |p| p['thought'] }
|
|
39
|
+
text = text_parts.filter_map { |p| p['text'] }.join
|
|
40
|
+
text.empty? ? nil : text
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def extract_thought_content(parts)
|
|
44
|
+
thought_parts = parts.select { |p| p['thought'] }
|
|
45
|
+
thoughts = thought_parts.filter_map { |p| p['text'] }.join
|
|
46
|
+
thoughts.empty? ? nil : thoughts
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def extract_thought_signature(parts)
|
|
50
|
+
parts.each do |part|
|
|
51
|
+
signature = part['thoughtSignature'] ||
|
|
52
|
+
part['thought_signature'] ||
|
|
53
|
+
part.dig('functionCall', 'thoughtSignature') ||
|
|
54
|
+
part.dig('functionCall', 'thought_signature')
|
|
55
|
+
return signature if signature
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
29
61
|
def extract_content(data)
|
|
30
62
|
return nil unless data['candidates']&.any?
|
|
31
63
|
|
|
@@ -52,7 +84,7 @@ module RubyLLM
|
|
|
52
84
|
error_data = JSON.parse(data)
|
|
53
85
|
[error_data['error']['code'], error_data['error']['message']]
|
|
54
86
|
rescue JSON::ParserError => e
|
|
55
|
-
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
|
87
|
+
RubyLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
|
|
56
88
|
[500, "Failed to parse error: #{data}"]
|
|
57
89
|
end
|
|
58
90
|
end
|
|
@@ -13,7 +13,7 @@ module RubyLLM
|
|
|
13
13
|
}]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def format_tool_call(msg)
|
|
16
|
+
def format_tool_call(msg) # rubocop:disable Metrics/PerceivedComplexity
|
|
17
17
|
parts = []
|
|
18
18
|
|
|
19
19
|
if msg.content && !(msg.content.respond_to?(:empty?) && msg.content.empty?)
|
|
@@ -21,13 +21,24 @@ module RubyLLM
|
|
|
21
21
|
parts.concat(formatted_content.is_a?(Array) ? formatted_content : [formatted_content])
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
fallback_signature = msg.thinking&.signature
|
|
25
|
+
used_fallback = false
|
|
26
|
+
|
|
24
27
|
msg.tool_calls.each_value do |tool_call|
|
|
25
|
-
|
|
28
|
+
part = {
|
|
26
29
|
functionCall: {
|
|
27
30
|
name: tool_call.name,
|
|
28
31
|
args: tool_call.arguments
|
|
29
32
|
}
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
signature = tool_call.thought_signature
|
|
36
|
+
if signature.nil? && fallback_signature && !used_fallback
|
|
37
|
+
signature = fallback_signature
|
|
38
|
+
used_fallback = true
|
|
39
|
+
end
|
|
40
|
+
part[:thoughtSignature] = signature if signature
|
|
41
|
+
parts << part
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
parts
|
|
@@ -61,11 +72,13 @@ module RubyLLM
|
|
|
61
72
|
next unless function_data
|
|
62
73
|
|
|
63
74
|
id = SecureRandom.uuid
|
|
75
|
+
thought_signature = part['thoughtSignature'] || part['thought_signature']
|
|
64
76
|
|
|
65
77
|
result[id] = ToolCall.new(
|
|
66
78
|
id:,
|
|
67
79
|
name: function_data['name'],
|
|
68
|
-
arguments: function_data['args'] || {}
|
|
80
|
+
arguments: function_data['args'] || {},
|
|
81
|
+
thought_signature: thought_signature
|
|
69
82
|
)
|
|
70
83
|
end
|
|
71
84
|
|
|
@@ -192,6 +205,25 @@ module RubyLLM
|
|
|
192
205
|
else 'STRING'
|
|
193
206
|
end
|
|
194
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
|
|
195
227
|
end
|
|
196
228
|
end
|
|
197
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
|
|
@@ -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
|
|
@@ -11,13 +11,71 @@ module RubyLLM
|
|
|
11
11
|
role.to_s
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def format_messages(messages)
|
|
15
|
+
messages.map do |msg|
|
|
16
|
+
{
|
|
17
|
+
role: format_role(msg.role),
|
|
18
|
+
content: format_content_with_thinking(msg),
|
|
19
|
+
tool_calls: OpenAI::Tools.format_tool_calls(msg.tool_calls),
|
|
20
|
+
tool_call_id: msg.tool_call_id
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
# rubocop:disable Metrics/ParameterLists
|
|
15
|
-
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)
|
|
16
28
|
payload = super
|
|
17
29
|
payload.delete(:stream_options)
|
|
30
|
+
payload.delete(:reasoning_effort)
|
|
31
|
+
warn_on_unsupported_thinking(model, thinking)
|
|
18
32
|
payload
|
|
19
33
|
end
|
|
20
34
|
# rubocop:enable Metrics/ParameterLists
|
|
35
|
+
|
|
36
|
+
def format_content_with_thinking(msg)
|
|
37
|
+
formatted_content = OpenAI::Media.format_content(msg.content)
|
|
38
|
+
return formatted_content unless msg.role == :assistant && msg.thinking
|
|
39
|
+
|
|
40
|
+
content_blocks = build_thinking_blocks(msg.thinking)
|
|
41
|
+
append_formatted_content(content_blocks, formatted_content)
|
|
42
|
+
|
|
43
|
+
content_blocks
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def warn_on_unsupported_thinking(model, thinking)
|
|
47
|
+
return unless thinking&.enabled?
|
|
48
|
+
return if model.id.to_s.include?('magistral')
|
|
49
|
+
|
|
50
|
+
RubyLLM.logger.warn(
|
|
51
|
+
'Mistral thinking is only supported on Magistral models. ' \
|
|
52
|
+
"Ignoring thinking settings for #{model.id}."
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_thinking_blocks(thinking)
|
|
57
|
+
return [] unless thinking
|
|
58
|
+
|
|
59
|
+
if thinking.text
|
|
60
|
+
[{
|
|
61
|
+
type: 'thinking',
|
|
62
|
+
thinking: [{ type: 'text', text: thinking.text }],
|
|
63
|
+
signature: thinking.signature
|
|
64
|
+
}.compact]
|
|
65
|
+
elsif thinking.signature
|
|
66
|
+
[{ type: 'thinking', signature: thinking.signature }]
|
|
67
|
+
else
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def append_formatted_content(content_blocks, formatted_content)
|
|
73
|
+
if formatted_content.is_a?(Array)
|
|
74
|
+
content_blocks.concat(formatted_content)
|
|
75
|
+
elsif formatted_content
|
|
76
|
+
content_blocks << { type: 'text', text: formatted_content }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
21
79
|
end
|
|
22
80
|
end
|
|
23
81
|
end
|
|
@@ -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
|