ruby_llm 1.6.3 → 1.7.0
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 +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +115 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/install_generator.rb +129 -33
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +137 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7_generator.rb +160 -0
- data/lib/ruby_llm/active_record/acts_as.rb +112 -319
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +58 -13
- data/lib/ruby_llm/attachment.rb +20 -0
- data/lib/ruby_llm/chat.rb +8 -7
- data/lib/ruby_llm/configuration.rb +9 -0
- data/lib/ruby_llm/connection.rb +4 -4
- data/lib/ruby_llm/model/info.rb +12 -0
- data/lib/ruby_llm/models.json +3579 -2029
- data/lib/ruby_llm/models.rb +51 -22
- data/lib/ruby_llm/provider.rb +3 -3
- data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
- data/lib/ruby_llm/providers/gemini/chat.rb +53 -25
- data/lib/ruby_llm/providers/gemini/media.rb +1 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +11 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +44 -8
- data/lib/ruby_llm/providers/gpustack.rb +1 -0
- data/lib/ruby_llm/providers/ollama/media.rb +2 -6
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +1 -0
- data/lib/ruby_llm/providers/openai/chat.rb +1 -1
- data/lib/ruby_llm/providers/openai/media.rb +4 -4
- data/lib/ruby_llm/providers/openai/tools.rb +11 -6
- data/lib/ruby_llm/providers/openai.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +20 -3
- data/lib/ruby_llm/streaming.rb +1 -1
- data/lib/ruby_llm/utils.rb +5 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -3
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +37 -2
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +2 -2
- metadata +37 -5
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
- data/lib/tasks/aliases.rake +0 -205
- data/lib/tasks/models_docs.rake +0 -214
- data/lib/tasks/models_update.rake +0 -108
data/lib/ruby_llm/models.rb
CHANGED
@@ -22,16 +22,20 @@ module RubyLLM
|
|
22
22
|
File.expand_path('models_schema.json', __dir__)
|
23
23
|
end
|
24
24
|
|
25
|
-
def refresh!
|
26
|
-
provider_models = fetch_from_providers
|
25
|
+
def refresh!(remote_only: false)
|
26
|
+
provider_models = fetch_from_providers(remote_only: remote_only)
|
27
27
|
parsera_models = fetch_from_parsera
|
28
28
|
merged_models = merge_models(provider_models, parsera_models)
|
29
29
|
@instance = new(merged_models)
|
30
30
|
end
|
31
31
|
|
32
|
-
def fetch_from_providers
|
32
|
+
def fetch_from_providers(remote_only: true)
|
33
33
|
config = RubyLLM.config
|
34
|
-
configured_classes =
|
34
|
+
configured_classes = if remote_only
|
35
|
+
Provider.configured_remote_providers(config)
|
36
|
+
else
|
37
|
+
Provider.configured_providers(config)
|
38
|
+
end
|
35
39
|
configured = configured_classes.map { |klass| klass.new(config) }
|
36
40
|
|
37
41
|
RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:name).join(', ')}"
|
@@ -54,14 +58,15 @@ module RubyLLM
|
|
54
58
|
provider_class ||= raise(Error, "Unknown provider: #{provider.to_sym}")
|
55
59
|
provider_instance = provider_class.new(config)
|
56
60
|
|
57
|
-
model =
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
model = if provider_instance.local?
|
62
|
+
begin
|
63
|
+
Models.find(model_id, provider)
|
64
|
+
rescue ModelNotFoundError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
model ||= Model::Info.default(model_id, provider_instance.slug)
|
65
70
|
else
|
66
71
|
model = Models.find model_id, provider
|
67
72
|
provider_class = Provider.providers[model.provider.to_sym] || raise(Error,
|
@@ -102,20 +107,36 @@ module RubyLLM
|
|
102
107
|
all_keys = parsera_by_key.keys | provider_by_key.keys
|
103
108
|
|
104
109
|
models = all_keys.map do |key|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
110
|
+
parsera_model = find_parsera_model(key, parsera_by_key)
|
111
|
+
provider_model = provider_by_key[key]
|
112
|
+
|
113
|
+
if parsera_model && provider_model
|
114
|
+
add_provider_metadata(parsera_model, provider_model)
|
115
|
+
elsif parsera_model
|
116
|
+
parsera_model
|
111
117
|
else
|
112
|
-
|
118
|
+
provider_model
|
113
119
|
end
|
114
120
|
end
|
115
121
|
|
116
122
|
models.sort_by { |m| [m.provider, m.id] }
|
117
123
|
end
|
118
124
|
|
125
|
+
def find_parsera_model(key, parsera_by_key)
|
126
|
+
# Direct match
|
127
|
+
return parsera_by_key[key] if parsera_by_key[key]
|
128
|
+
|
129
|
+
# VertexAI uses same models as Gemini
|
130
|
+
provider, model_id = key.split(':', 2)
|
131
|
+
return unless provider == 'vertexai'
|
132
|
+
|
133
|
+
gemini_model = parsera_by_key["gemini:#{model_id}"]
|
134
|
+
return unless gemini_model
|
135
|
+
|
136
|
+
# Return Gemini's Parsera data but with VertexAI as provider
|
137
|
+
Model::Info.new(gemini_model.to_h.merge(provider: 'vertexai'))
|
138
|
+
end
|
139
|
+
|
119
140
|
def index_by_key(models)
|
120
141
|
models.each_with_object({}) do |model, hash|
|
121
142
|
hash["#{model.provider}:#{model.id}"] = model
|
@@ -134,13 +155,21 @@ module RubyLLM
|
|
134
155
|
end
|
135
156
|
|
136
157
|
def load_models
|
158
|
+
read_from_json
|
159
|
+
end
|
160
|
+
|
161
|
+
def load_from_json!
|
162
|
+
@models = read_from_json
|
163
|
+
end
|
164
|
+
|
165
|
+
def read_from_json
|
137
166
|
data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
|
138
167
|
JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
|
139
168
|
rescue JSON::ParserError
|
140
169
|
[]
|
141
170
|
end
|
142
171
|
|
143
|
-
def
|
172
|
+
def save_to_json
|
144
173
|
File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
|
145
174
|
end
|
146
175
|
|
@@ -184,8 +213,8 @@ module RubyLLM
|
|
184
213
|
self.class.new(all.select { |m| m.provider == provider.to_s })
|
185
214
|
end
|
186
215
|
|
187
|
-
def refresh!
|
188
|
-
self.class.refresh!
|
216
|
+
def refresh!(remote_only: false)
|
217
|
+
self.class.refresh!(remote_only: remote_only)
|
189
218
|
end
|
190
219
|
|
191
220
|
private
|
data/lib/ruby_llm/provider.rb
CHANGED
@@ -41,7 +41,6 @@ module RubyLLM
|
|
41
41
|
normalized_temperature = maybe_normalize_temperature(temperature, model)
|
42
42
|
|
43
43
|
payload = Utils.deep_merge(
|
44
|
-
params,
|
45
44
|
render_payload(
|
46
45
|
messages,
|
47
46
|
tools: tools,
|
@@ -49,7 +48,8 @@ module RubyLLM
|
|
49
48
|
model: model,
|
50
49
|
stream: block_given?,
|
51
50
|
schema: schema
|
52
|
-
)
|
51
|
+
),
|
52
|
+
params
|
53
53
|
)
|
54
54
|
|
55
55
|
if block_given?
|
@@ -201,7 +201,7 @@ module RubyLLM
|
|
201
201
|
raise ConfigurationError, "Missing configuration for #{name}: #{missing.join(', ')}"
|
202
202
|
end
|
203
203
|
|
204
|
-
def maybe_normalize_temperature(temperature,
|
204
|
+
def maybe_normalize_temperature(temperature, _model)
|
205
205
|
temperature
|
206
206
|
end
|
207
207
|
|
@@ -37,10 +37,10 @@ module RubyLLM
|
|
37
37
|
|
38
38
|
def build_base_payload(chat_messages, model, stream)
|
39
39
|
{
|
40
|
-
model: model,
|
40
|
+
model: model.id,
|
41
41
|
messages: chat_messages.map { |msg| format_message(msg) },
|
42
42
|
stream: stream,
|
43
|
-
max_tokens:
|
43
|
+
max_tokens: model.max_tokens || 4096
|
44
44
|
}
|
45
45
|
end
|
46
46
|
|
@@ -40,7 +40,7 @@ module RubyLLM
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
|
43
|
-
@model_id = model
|
43
|
+
@model_id = model.id
|
44
44
|
|
45
45
|
system_messages, chat_messages = Anthropic::Chat.separate_messages(messages)
|
46
46
|
system_content = Anthropic::Chat.build_system_content(system_messages)
|
@@ -54,7 +54,7 @@ module RubyLLM
|
|
54
54
|
{
|
55
55
|
anthropic_version: 'bedrock-2023-05-31',
|
56
56
|
messages: chat_messages.map { |msg| format_message(msg) },
|
57
|
-
max_tokens:
|
57
|
+
max_tokens: model.max_tokens || 4096
|
58
58
|
}
|
59
59
|
end
|
60
60
|
end
|
@@ -72,7 +72,25 @@ module RubyLLM
|
|
72
72
|
return model_id unless model_data['inferenceTypesSupported']&.include?('INFERENCE_PROFILE')
|
73
73
|
return model_id if model_data['inferenceTypesSupported']&.include?('ON_DEMAND')
|
74
74
|
|
75
|
-
|
75
|
+
desired_region_prefix = inference_profile_region_prefix
|
76
|
+
|
77
|
+
# Return unchanged if model already has the correct region prefix
|
78
|
+
return model_id if model_id.start_with?("#{desired_region_prefix}.")
|
79
|
+
|
80
|
+
# Remove any existing region prefix (e.g., "us.", "eu.", "ap.")
|
81
|
+
clean_model_id = model_id.sub(/^[a-z]{2}\./, '')
|
82
|
+
|
83
|
+
# Apply the desired region prefix
|
84
|
+
"#{desired_region_prefix}.#{clean_model_id}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def inference_profile_region_prefix
|
88
|
+
# Extract region prefix from bedrock_region (e.g., "eu-west-3" -> "eu")
|
89
|
+
region = @config.bedrock_region.to_s
|
90
|
+
return 'us' if region.empty? # Default fallback
|
91
|
+
|
92
|
+
# Take first two characters as the region prefix
|
93
|
+
region[0, 2]
|
76
94
|
end
|
77
95
|
end
|
78
96
|
end
|
@@ -12,7 +12,7 @@ module RubyLLM
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
15
|
-
@model = model
|
15
|
+
@model = model.id
|
16
16
|
payload = {
|
17
17
|
contents: format_messages(messages),
|
18
18
|
generationConfig: {}
|
@@ -62,7 +62,7 @@ module RubyLLM
|
|
62
62
|
name: msg.tool_call_id,
|
63
63
|
response: {
|
64
64
|
name: msg.tool_call_id,
|
65
|
-
content: msg.content
|
65
|
+
content: Media.format_content(msg.content)
|
66
66
|
}
|
67
67
|
}
|
68
68
|
}]
|
@@ -86,31 +86,12 @@ module RubyLLM
|
|
86
86
|
)
|
87
87
|
end
|
88
88
|
|
89
|
-
def convert_schema_to_gemini(schema)
|
89
|
+
def convert_schema_to_gemini(schema)
|
90
90
|
return nil unless schema
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
type: 'OBJECT',
|
96
|
-
properties: schema[:properties]&.transform_values { |prop| convert_schema_to_gemini(prop) } || {},
|
97
|
-
required: schema[:required] || []
|
98
|
-
}
|
99
|
-
when 'array'
|
100
|
-
{
|
101
|
-
type: 'ARRAY',
|
102
|
-
items: schema[:items] ? convert_schema_to_gemini(schema[:items]) : { type: 'STRING' }
|
103
|
-
}
|
104
|
-
when 'string'
|
105
|
-
result = { type: 'STRING' }
|
106
|
-
result[:enum] = schema[:enum] if schema[:enum]
|
107
|
-
result
|
108
|
-
when 'number', 'integer'
|
109
|
-
{ type: 'NUMBER' }
|
110
|
-
when 'boolean'
|
111
|
-
{ type: 'BOOLEAN' }
|
112
|
-
else
|
113
|
-
{ type: 'STRING' }
|
92
|
+
build_base_schema(schema).tap do |result|
|
93
|
+
result[:description] = schema[:description] if schema[:description]
|
94
|
+
apply_type_specific_attributes(result, schema)
|
114
95
|
end
|
115
96
|
end
|
116
97
|
|
@@ -137,6 +118,53 @@ module RubyLLM
|
|
137
118
|
thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
|
138
119
|
candidates + thoughts
|
139
120
|
end
|
121
|
+
|
122
|
+
def build_base_schema(schema)
|
123
|
+
case schema[:type]
|
124
|
+
when 'object'
|
125
|
+
build_object_schema(schema)
|
126
|
+
when 'array'
|
127
|
+
{ type: 'ARRAY', items: schema[:items] ? convert_schema_to_gemini(schema[:items]) : { type: 'STRING' } }
|
128
|
+
when 'number'
|
129
|
+
{ type: 'NUMBER' }
|
130
|
+
when 'integer'
|
131
|
+
{ type: 'INTEGER' }
|
132
|
+
when 'boolean'
|
133
|
+
{ type: 'BOOLEAN' }
|
134
|
+
else
|
135
|
+
{ type: 'STRING' }
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_object_schema(schema)
|
140
|
+
{
|
141
|
+
type: 'OBJECT',
|
142
|
+
properties: (schema[:properties] || {}).transform_values { |prop| convert_schema_to_gemini(prop) },
|
143
|
+
required: schema[:required] || []
|
144
|
+
}.tap do |object|
|
145
|
+
object[:propertyOrdering] = schema[:propertyOrdering] if schema[:propertyOrdering]
|
146
|
+
object[:nullable] = schema[:nullable] if schema.key?(:nullable)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def apply_type_specific_attributes(result, schema)
|
151
|
+
case schema[:type]
|
152
|
+
when 'string'
|
153
|
+
copy_attributes(result, schema, :enum, :format, :nullable)
|
154
|
+
when 'number', 'integer'
|
155
|
+
copy_attributes(result, schema, :format, :minimum, :maximum, :enum, :nullable)
|
156
|
+
when 'array'
|
157
|
+
copy_attributes(result, schema, :minItems, :maxItems, :nullable)
|
158
|
+
when 'boolean'
|
159
|
+
copy_attributes(result, schema, :nullable)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def copy_attributes(target, source, *attributes)
|
164
|
+
attributes.each do |attr|
|
165
|
+
target[attr] = source[attr] if attr == :nullable ? source.key?(attr) : source[attr]
|
166
|
+
end
|
167
|
+
end
|
140
168
|
end
|
141
169
|
end
|
142
170
|
end
|
@@ -7,6 +7,17 @@ module RubyLLM
|
|
7
7
|
module Chat
|
8
8
|
module_function
|
9
9
|
|
10
|
+
def format_messages(messages)
|
11
|
+
messages.map do |msg|
|
12
|
+
{
|
13
|
+
role: format_role(msg.role),
|
14
|
+
content: GPUStack::Media.format_content(msg.content),
|
15
|
+
tool_calls: format_tool_calls(msg.tool_calls),
|
16
|
+
tool_call_id: msg.tool_call_id
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
10
21
|
def format_role(role)
|
11
22
|
role.to_s
|
12
23
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class GPUStack
|
6
|
+
# Handles formatting of media content (images, audio) for GPUStack APIs
|
7
|
+
module Media
|
8
|
+
extend OpenAI::Media
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def format_content(content)
|
13
|
+
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
14
|
+
return content unless content.is_a?(Content)
|
15
|
+
|
16
|
+
parts = []
|
17
|
+
parts << format_text(content.text) if content.text
|
18
|
+
|
19
|
+
content.attachments.each do |attachment|
|
20
|
+
case attachment.type
|
21
|
+
when :image
|
22
|
+
parts << GPUStack::Media.format_image(attachment)
|
23
|
+
when :text
|
24
|
+
parts << format_text_file(attachment)
|
25
|
+
else
|
26
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
parts
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_image(image)
|
34
|
+
{
|
35
|
+
type: 'image_url',
|
36
|
+
image_url: {
|
37
|
+
url: image.for_llm,
|
38
|
+
detail: 'auto'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -16,10 +16,10 @@ module RubyLLM
|
|
16
16
|
items.map do |model|
|
17
17
|
Model::Info.new(
|
18
18
|
id: model['name'],
|
19
|
+
name: model['name'],
|
19
20
|
created_at: model['created_at'] ? Time.parse(model['created_at']) : nil,
|
20
|
-
display_name: "#{model['source']}/#{model['name']}",
|
21
21
|
provider: slug,
|
22
|
-
|
22
|
+
family: 'gpustack',
|
23
23
|
metadata: {
|
24
24
|
description: model['description'],
|
25
25
|
source: model['source'],
|
@@ -30,12 +30,10 @@ module RubyLLM
|
|
30
30
|
categories: model['categories']
|
31
31
|
},
|
32
32
|
context_window: model.dig('meta', 'n_ctx'),
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
input_price_per_million: 0.0,
|
38
|
-
output_price_per_million: 0.0
|
33
|
+
max_output_tokens: model.dig('meta', 'n_ctx'),
|
34
|
+
capabilities: build_capabilities(model),
|
35
|
+
modalities: build_modalities(model),
|
36
|
+
pricing: {}
|
39
37
|
)
|
40
38
|
end
|
41
39
|
end
|
@@ -48,6 +46,44 @@ module RubyLLM
|
|
48
46
|
|
49
47
|
'other'
|
50
48
|
end
|
49
|
+
|
50
|
+
def build_capabilities(model) # rubocop:disable Metrics/PerceivedComplexity
|
51
|
+
capabilities = []
|
52
|
+
|
53
|
+
# Add streaming by default for LLM models
|
54
|
+
capabilities << 'streaming' if model['categories']&.include?('llm')
|
55
|
+
|
56
|
+
# Map GPUStack metadata to standard capabilities
|
57
|
+
capabilities << 'function_calling' if model.dig('meta', 'support_tool_calls')
|
58
|
+
capabilities << 'vision' if model.dig('meta', 'support_vision')
|
59
|
+
capabilities << 'reasoning' if model.dig('meta', 'support_reasoning')
|
60
|
+
|
61
|
+
# GPUStack models generally support structured output and json mode
|
62
|
+
capabilities << 'structured_output' if model['categories']&.include?('llm')
|
63
|
+
capabilities << 'json_mode' if model['categories']&.include?('llm')
|
64
|
+
|
65
|
+
capabilities
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_modalities(model)
|
69
|
+
input_modalities = []
|
70
|
+
output_modalities = []
|
71
|
+
|
72
|
+
if model['categories']&.include?('llm')
|
73
|
+
input_modalities << 'text'
|
74
|
+
input_modalities << 'image' if model.dig('meta', 'support_vision')
|
75
|
+
input_modalities << 'audio' if model.dig('meta', 'support_audio')
|
76
|
+
output_modalities << 'text'
|
77
|
+
elsif model['categories']&.include?('embedding')
|
78
|
+
input_modalities << 'text'
|
79
|
+
output_modalities << 'embeddings'
|
80
|
+
end
|
81
|
+
|
82
|
+
{
|
83
|
+
input: input_modalities,
|
84
|
+
output: output_modalities
|
85
|
+
}
|
86
|
+
end
|
51
87
|
end
|
52
88
|
end
|
53
89
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module Providers
|
5
5
|
class Ollama
|
6
|
-
# Handles formatting of media content (images, audio) for
|
6
|
+
# Handles formatting of media content (images, audio) for Ollama APIs
|
7
7
|
module Media
|
8
8
|
extend OpenAI::Media
|
9
9
|
|
@@ -20,10 +20,6 @@ module RubyLLM
|
|
20
20
|
case attachment.type
|
21
21
|
when :image
|
22
22
|
parts << Ollama::Media.format_image(attachment)
|
23
|
-
when :pdf
|
24
|
-
parts << format_pdf(attachment)
|
25
|
-
when :audio
|
26
|
-
parts << format_audio(attachment)
|
27
23
|
when :text
|
28
24
|
parts << format_text_file(attachment)
|
29
25
|
else
|
@@ -38,7 +34,7 @@ module RubyLLM
|
|
38
34
|
{
|
39
35
|
type: 'image_url',
|
40
36
|
image_url: {
|
41
|
-
url:
|
37
|
+
url: image.for_llm,
|
42
38
|
detail: 'auto'
|
43
39
|
}
|
44
40
|
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class Ollama
|
6
|
+
# Models methods for the Ollama API integration
|
7
|
+
module Models
|
8
|
+
def models_url
|
9
|
+
'models'
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_list_models_response(response, slug, _capabilities)
|
13
|
+
data = response.body['data'] || []
|
14
|
+
data.map do |model|
|
15
|
+
Model::Info.new(
|
16
|
+
id: model['id'],
|
17
|
+
name: model['id'],
|
18
|
+
provider: slug,
|
19
|
+
family: 'ollama',
|
20
|
+
created_at: model['created'] ? Time.at(model['created']) : nil,
|
21
|
+
modalities: {
|
22
|
+
input: %w[text image],
|
23
|
+
output: %w[text]
|
24
|
+
},
|
25
|
+
capabilities: %w[streaming function_calling structured_output vision],
|
26
|
+
pricing: {},
|
27
|
+
metadata: {
|
28
|
+
owned_by: model['owned_by']
|
29
|
+
}
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -13,7 +13,7 @@ module RubyLLM
|
|
13
13
|
|
14
14
|
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists
|
15
15
|
payload = {
|
16
|
-
model: model,
|
16
|
+
model: model.id,
|
17
17
|
messages: format_messages(messages),
|
18
18
|
stream: stream
|
19
19
|
}
|
@@ -36,7 +36,7 @@ module RubyLLM
|
|
36
36
|
{
|
37
37
|
type: 'image_url',
|
38
38
|
image_url: {
|
39
|
-
url: image.url? ? image.source :
|
39
|
+
url: image.url? ? image.source : image.for_llm
|
40
40
|
}
|
41
41
|
}
|
42
42
|
end
|
@@ -46,7 +46,7 @@ module RubyLLM
|
|
46
46
|
type: 'file',
|
47
47
|
file: {
|
48
48
|
filename: pdf.filename,
|
49
|
-
file_data:
|
49
|
+
file_data: pdf.for_llm
|
50
50
|
}
|
51
51
|
}
|
52
52
|
end
|
@@ -54,7 +54,7 @@ module RubyLLM
|
|
54
54
|
def format_text_file(text_file)
|
55
55
|
{
|
56
56
|
type: 'text',
|
57
|
-
text:
|
57
|
+
text: text_file.for_llm
|
58
58
|
}
|
59
59
|
end
|
60
60
|
|
@@ -63,7 +63,7 @@ module RubyLLM
|
|
63
63
|
type: 'input_audio',
|
64
64
|
input_audio: {
|
65
65
|
data: audio.encoded,
|
66
|
-
format: audio.
|
66
|
+
format: audio.format
|
67
67
|
}
|
68
68
|
}
|
69
69
|
end
|
@@ -44,6 +44,16 @@ module RubyLLM
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
def parse_tool_call_arguments(tool_call)
|
48
|
+
arguments = tool_call.dig('function', 'arguments')
|
49
|
+
|
50
|
+
if arguments.nil? || arguments.empty?
|
51
|
+
{}
|
52
|
+
else
|
53
|
+
JSON.parse(arguments)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
47
57
|
def parse_tool_calls(tool_calls, parse_arguments: true)
|
48
58
|
return nil unless tool_calls&.any?
|
49
59
|
|
@@ -54,12 +64,7 @@ module RubyLLM
|
|
54
64
|
id: tc['id'],
|
55
65
|
name: tc.dig('function', 'name'),
|
56
66
|
arguments: if parse_arguments
|
57
|
-
|
58
|
-
{}
|
59
|
-
else
|
60
|
-
JSON.parse(tc.dig('function',
|
61
|
-
'arguments'))
|
62
|
-
end
|
67
|
+
parse_tool_call_arguments(tc)
|
63
68
|
else
|
64
69
|
tc.dig('function', 'arguments')
|
65
70
|
end
|
@@ -24,8 +24,8 @@ module RubyLLM
|
|
24
24
|
}.compact
|
25
25
|
end
|
26
26
|
|
27
|
-
def maybe_normalize_temperature(temperature,
|
28
|
-
OpenAI::Capabilities.normalize_temperature(temperature,
|
27
|
+
def maybe_normalize_temperature(temperature, model)
|
28
|
+
OpenAI::Capabilities.normalize_temperature(temperature, model.id)
|
29
29
|
end
|
30
30
|
|
31
31
|
class << self
|