ruby_llm_community 0.0.6 → 1.0.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 +3 -3
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +34 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +5 -0
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +6 -0
- data/lib/generators/ruby_llm/install_generator.rb +27 -2
- data/lib/ruby_llm/active_record/acts_as.rb +163 -24
- data/lib/ruby_llm/aliases.json +58 -5
- data/lib/ruby_llm/aliases.rb +7 -25
- data/lib/ruby_llm/chat.rb +10 -17
- data/lib/ruby_llm/configuration.rb +5 -12
- data/lib/ruby_llm/connection.rb +4 -4
- data/lib/ruby_llm/connection_multipart.rb +19 -0
- data/lib/ruby_llm/content.rb +5 -2
- data/lib/ruby_llm/embedding.rb +1 -2
- data/lib/ruby_llm/error.rb +0 -8
- data/lib/ruby_llm/image.rb +23 -8
- data/lib/ruby_llm/image_attachment.rb +21 -0
- data/lib/ruby_llm/message.rb +6 -6
- data/lib/ruby_llm/model/info.rb +12 -10
- data/lib/ruby_llm/model/pricing.rb +0 -3
- data/lib/ruby_llm/model/pricing_category.rb +0 -2
- data/lib/ruby_llm/model/pricing_tier.rb +0 -1
- data/lib/ruby_llm/models.json +2147 -470
- data/lib/ruby_llm/models.rb +65 -34
- data/lib/ruby_llm/provider.rb +8 -8
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
- data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
- data/lib/ruby_llm/providers/anthropic.rb +1 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -4
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
- data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
- data/lib/ruby_llm/providers/bedrock.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +28 -100
- data/lib/ruby_llm/providers/gemini/chat.rb +57 -29
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
- data/lib/ruby_llm/providers/gemini/images.rb +1 -2
- data/lib/ruby_llm/providers/gemini/media.rb +0 -1
- data/lib/ruby_llm/providers/gemini/models.rb +1 -2
- data/lib/ruby_llm/providers/gemini/streaming.rb +15 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
- data/lib/ruby_llm/providers/gpustack/chat.rb +11 -1
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +44 -9
- data/lib/ruby_llm/providers/gpustack.rb +1 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
- data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
- data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/mistral/models.rb +0 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
- data/lib/ruby_llm/providers/ollama/media.rb +1 -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/capabilities.rb +3 -16
- data/lib/ruby_llm/providers/openai/chat.rb +1 -3
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/openai/images.rb +73 -3
- data/lib/ruby_llm/providers/openai/media.rb +0 -1
- data/lib/ruby_llm/providers/openai/response.rb +120 -29
- data/lib/ruby_llm/providers/openai/response_media.rb +2 -2
- data/lib/ruby_llm/providers/openai/streaming.rb +107 -47
- data/lib/ruby_llm/providers/openai/tools.rb +1 -1
- data/lib/ruby_llm/providers/openai.rb +1 -3
- data/lib/ruby_llm/providers/openai_base.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
- data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
- data/lib/ruby_llm/providers/perplexity.rb +1 -5
- 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 +0 -1
- data/lib/ruby_llm/stream_accumulator.rb +72 -10
- data/lib/ruby_llm/streaming.rb +16 -25
- data/lib/ruby_llm/tool.rb +2 -19
- data/lib/ruby_llm/tool_call.rb +0 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm_community.rb +5 -3
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +37 -2
- data/lib/tasks/vcr.rake +0 -7
- metadata +13 -4
- data/lib/tasks/aliases.rake +0 -235
- data/lib/tasks/models_docs.rake +0 -224
- data/lib/tasks/models_update.rake +0 -108
@@ -13,7 +13,7 @@ module RubyLLM
|
|
13
13
|
|
14
14
|
def render_response_payload(messages, tools:, temperature:, model:, cache_prompts:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
15
15
|
payload = {
|
16
|
-
model: model,
|
16
|
+
model: model.id,
|
17
17
|
input: format_input(messages),
|
18
18
|
stream: stream
|
19
19
|
}
|
@@ -40,39 +40,72 @@ module RubyLLM
|
|
40
40
|
payload
|
41
41
|
end
|
42
42
|
|
43
|
-
def format_input(messages)
|
43
|
+
def format_input(messages)
|
44
44
|
all_tool_calls = messages.flat_map do |m|
|
45
45
|
m.tool_calls&.values || []
|
46
46
|
end
|
47
|
-
messages.flat_map
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
{
|
60
|
-
type: 'function_call_output',
|
61
|
-
call_id: all_tool_calls.detect { |tc| tc.id == msg.tool_call_id }&.id,
|
62
|
-
output: msg.content,
|
63
|
-
status: 'completed'
|
64
|
-
}
|
65
|
-
else
|
66
|
-
{
|
67
|
-
type: 'message',
|
68
|
-
role: format_role(msg.role),
|
69
|
-
content: ResponseMedia.format_content(msg.content),
|
70
|
-
status: 'completed'
|
71
|
-
}.compact
|
72
|
-
end
|
47
|
+
messages.flat_map { |msg| format_message_input(msg, all_tool_calls) }.flatten
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_message_input(msg, all_tool_calls)
|
51
|
+
if msg.tool_call?
|
52
|
+
format_tool_call_message(msg)
|
53
|
+
elsif msg.role == :tool
|
54
|
+
format_tool_response_message(msg, all_tool_calls)
|
55
|
+
elsif assistant_message_with_image_attachment?(msg)
|
56
|
+
format_image_generation_message(msg)
|
57
|
+
else
|
58
|
+
format_regular_message(msg)
|
73
59
|
end
|
74
60
|
end
|
75
61
|
|
62
|
+
def format_tool_call_message(msg)
|
63
|
+
msg.tool_calls.map do |_, tc|
|
64
|
+
{
|
65
|
+
type: 'function_call',
|
66
|
+
call_id: tc.id,
|
67
|
+
name: tc.name,
|
68
|
+
arguments: JSON.generate(tc.arguments),
|
69
|
+
status: 'completed'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def format_tool_response_message(msg, all_tool_calls)
|
75
|
+
{
|
76
|
+
type: 'function_call_output',
|
77
|
+
call_id: all_tool_calls.detect { |tc| tc.id == msg.tool_call_id }&.id,
|
78
|
+
output: msg.content,
|
79
|
+
status: 'completed'
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def format_image_generation_message(msg)
|
84
|
+
items = []
|
85
|
+
image_attachment = msg.content.attachments.first
|
86
|
+
if image_attachment.reasoning_id
|
87
|
+
items << {
|
88
|
+
type: 'reasoning',
|
89
|
+
id: image_attachment.reasoning_id,
|
90
|
+
summary: []
|
91
|
+
}
|
92
|
+
end
|
93
|
+
items << {
|
94
|
+
type: 'image_generation_call',
|
95
|
+
id: image_attachment.id
|
96
|
+
}
|
97
|
+
items
|
98
|
+
end
|
99
|
+
|
100
|
+
def format_regular_message(msg)
|
101
|
+
{
|
102
|
+
type: 'message',
|
103
|
+
role: format_role(msg.role),
|
104
|
+
content: ResponseMedia.format_content(msg.content),
|
105
|
+
status: 'completed'
|
106
|
+
}.compact
|
107
|
+
end
|
108
|
+
|
76
109
|
def format_role(role)
|
77
110
|
case role
|
78
111
|
when :system
|
@@ -93,16 +126,62 @@ module RubyLLM
|
|
93
126
|
|
94
127
|
Message.new(
|
95
128
|
role: :assistant,
|
96
|
-
content:
|
129
|
+
content: all_output_content(outputs),
|
97
130
|
tool_calls: parse_response_tool_calls(outputs),
|
98
131
|
input_tokens: data['usage']['input_tokens'],
|
99
132
|
output_tokens: data['usage']['output_tokens'],
|
100
133
|
cached_tokens: data.dig('usage', 'input_tokens_details', 'cached_tokens'),
|
101
134
|
model_id: data['model'],
|
135
|
+
reasoning_id: extract_reasoning_id(outputs),
|
102
136
|
raw: response
|
103
137
|
)
|
104
138
|
end
|
105
139
|
|
140
|
+
def all_output_content(outputs)
|
141
|
+
@current_outputs = outputs
|
142
|
+
text_content = extract_text_content(outputs)
|
143
|
+
image_outputs = outputs.select { |o| o['type'] == 'image_generation_call' }
|
144
|
+
|
145
|
+
return text_content unless image_outputs.any?
|
146
|
+
|
147
|
+
build_content_with_images(text_content, image_outputs)
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def extract_text_content(outputs)
|
153
|
+
outputs.select { |o| o['type'] == 'message' }.flat_map do |o|
|
154
|
+
o['content'].filter_map do |c|
|
155
|
+
c['type'] == 'output_text' && c['text']
|
156
|
+
end
|
157
|
+
end.join("\n")
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_content_with_images(text_content, image_outputs)
|
161
|
+
content = RubyLLM::Content.new(text_content)
|
162
|
+
reasoning_id = extract_reasoning_id(@current_outputs)
|
163
|
+
image_outputs.each do |output|
|
164
|
+
attach_image_to_content(content, output, reasoning_id)
|
165
|
+
end
|
166
|
+
content
|
167
|
+
end
|
168
|
+
|
169
|
+
def attach_image_to_content(content, output, reasoning_id)
|
170
|
+
image_data = output['result']
|
171
|
+
output_format = output['output_format'] || 'png'
|
172
|
+
mime_type = "image/#{output_format}"
|
173
|
+
|
174
|
+
content.attach(
|
175
|
+
RubyLLM::ImageAttachment.new(
|
176
|
+
data: image_data,
|
177
|
+
mime_type: mime_type,
|
178
|
+
model_id: nil,
|
179
|
+
id: output['id'],
|
180
|
+
reasoning_id: reasoning_id
|
181
|
+
)
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
106
185
|
def all_output_text(outputs)
|
107
186
|
outputs.select { |o| o['type'] == 'message' }.flat_map do |o|
|
108
187
|
o['content'].filter_map do |c|
|
@@ -110,6 +189,18 @@ module RubyLLM
|
|
110
189
|
end
|
111
190
|
end.join("\n")
|
112
191
|
end
|
192
|
+
|
193
|
+
def assistant_message_with_image_attachment?(msg)
|
194
|
+
msg.role == :assistant &&
|
195
|
+
msg.content.is_a?(RubyLLM::Content) &&
|
196
|
+
msg.content.attachments.any? &&
|
197
|
+
msg.content.attachments.first.is_a?(RubyLLM::ImageAttachment)
|
198
|
+
end
|
199
|
+
|
200
|
+
def extract_reasoning_id(outputs)
|
201
|
+
reasoning_item = outputs.find { |o| o['type'] == 'reasoning' }
|
202
|
+
reasoning_item&.dig('id')
|
203
|
+
end
|
113
204
|
end
|
114
205
|
end
|
115
206
|
end
|
@@ -7,12 +7,12 @@ module RubyLLM
|
|
7
7
|
module ResponseMedia
|
8
8
|
module_function
|
9
9
|
|
10
|
-
def format_content(content)
|
10
|
+
def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
|
11
11
|
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
12
12
|
return content unless content.is_a?(Content)
|
13
13
|
|
14
14
|
parts = []
|
15
|
-
parts << format_text(content.text) if content.text
|
15
|
+
parts << format_text(content.text) if content.text && !content.text.empty?
|
16
16
|
|
17
17
|
content.attachments.each do |attachment|
|
18
18
|
case attachment.type
|
@@ -26,60 +26,65 @@ module RubyLLM
|
|
26
26
|
|
27
27
|
def build_responses_chunk(data)
|
28
28
|
case data['type']
|
29
|
-
when 'response.text.delta'
|
30
|
-
# Text content delta - deprecated format
|
31
|
-
Chunk.new(
|
32
|
-
role: :assistant,
|
33
|
-
model_id: data.dig('response', 'model'),
|
34
|
-
content: data['delta'],
|
35
|
-
tool_calls: nil,
|
36
|
-
input_tokens: nil,
|
37
|
-
output_tokens: nil
|
38
|
-
)
|
39
29
|
when 'response.output_text.delta'
|
40
|
-
|
41
|
-
Chunk.new(
|
42
|
-
role: :assistant,
|
43
|
-
model_id: nil, # Model is in the completion event
|
44
|
-
content: data['delta'],
|
45
|
-
tool_calls: nil,
|
46
|
-
input_tokens: nil,
|
47
|
-
output_tokens: nil
|
48
|
-
)
|
30
|
+
build_text_delta_chunk(data)
|
49
31
|
when 'response.function_call_arguments.delta'
|
50
|
-
# Tool call arguments delta - handled by accumulator
|
51
|
-
# We need to track these deltas to build up the complete tool call
|
52
32
|
build_tool_call_delta_chunk(data)
|
33
|
+
when 'response.image_generation_call.partial_image'
|
34
|
+
build_partial_image_chunk(data)
|
53
35
|
when 'response.output_item.added'
|
54
|
-
|
55
|
-
if data.dig('item', 'type') == 'function_call'
|
56
|
-
build_tool_call_start_chunk(data)
|
57
|
-
else
|
58
|
-
build_empty_chunk(data)
|
59
|
-
end
|
36
|
+
handle_output_item_added(data)
|
60
37
|
when 'response.output_item.done'
|
61
|
-
|
62
|
-
if data.dig('item', 'type') == 'function_call'
|
63
|
-
build_tool_call_complete_chunk(data)
|
64
|
-
else
|
65
|
-
build_empty_chunk(data)
|
66
|
-
end
|
38
|
+
handle_output_item_done(data)
|
67
39
|
when 'response.completed'
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
40
|
+
build_completion_chunk(data)
|
41
|
+
else
|
42
|
+
build_empty_chunk(data)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_text_delta_chunk(data)
|
47
|
+
Chunk.new(
|
48
|
+
role: :assistant,
|
49
|
+
model_id: nil,
|
50
|
+
content: data['delta'],
|
51
|
+
tool_calls: nil,
|
52
|
+
input_tokens: nil,
|
53
|
+
output_tokens: nil
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_output_item_added(data)
|
58
|
+
if data.dig('item', 'type') == 'function_call'
|
59
|
+
build_tool_call_start_chunk(data)
|
60
|
+
elsif data.dig('item', 'type') == 'reasoning'
|
61
|
+
build_reasoning_chunk(data)
|
77
62
|
else
|
78
|
-
# Other event types (response.created, response.in_progress, etc.)
|
79
63
|
build_empty_chunk(data)
|
80
64
|
end
|
81
65
|
end
|
82
66
|
|
67
|
+
def handle_output_item_done(data)
|
68
|
+
if data.dig('item', 'type') == 'function_call'
|
69
|
+
build_tool_call_complete_chunk(data)
|
70
|
+
elsif data.dig('item', 'type') == 'image_generation_call'
|
71
|
+
build_completed_image_chunk(data)
|
72
|
+
else
|
73
|
+
build_empty_chunk(data)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_completion_chunk(data)
|
78
|
+
Chunk.new(
|
79
|
+
role: :assistant,
|
80
|
+
model_id: data.dig('response', 'model'),
|
81
|
+
content: nil,
|
82
|
+
tool_calls: nil,
|
83
|
+
input_tokens: data.dig('response', 'usage', 'input_tokens'),
|
84
|
+
output_tokens: data.dig('response', 'usage', 'output_tokens')
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
83
88
|
def build_chat_completions_chunk(data)
|
84
89
|
Chunk.new(
|
85
90
|
role: :assistant,
|
@@ -93,8 +98,6 @@ module RubyLLM
|
|
93
98
|
end
|
94
99
|
|
95
100
|
def build_tool_call_delta_chunk(data)
|
96
|
-
# For tool call argument deltas, we need to create a partial tool call
|
97
|
-
# The accumulator will handle building up the complete arguments
|
98
101
|
tool_call_data = {
|
99
102
|
'id' => data['item_id'],
|
100
103
|
'function' => {
|
@@ -153,10 +156,10 @@ module RubyLLM
|
|
153
156
|
)
|
154
157
|
end
|
155
158
|
|
156
|
-
def build_empty_chunk(
|
159
|
+
def build_empty_chunk(_data)
|
157
160
|
Chunk.new(
|
158
161
|
role: :assistant,
|
159
|
-
model_id:
|
162
|
+
model_id: nil,
|
160
163
|
content: nil,
|
161
164
|
tool_calls: nil,
|
162
165
|
input_tokens: nil,
|
@@ -164,6 +167,63 @@ module RubyLLM
|
|
164
167
|
)
|
165
168
|
end
|
166
169
|
|
170
|
+
def build_partial_image_chunk(data)
|
171
|
+
content = build_image_content(data['partial_image_b64'], 'image/png', nil, nil)
|
172
|
+
|
173
|
+
Chunk.new(
|
174
|
+
role: :assistant,
|
175
|
+
model_id: nil,
|
176
|
+
content: content,
|
177
|
+
tool_calls: nil,
|
178
|
+
input_tokens: nil,
|
179
|
+
output_tokens: nil
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
def build_completed_image_chunk(data)
|
184
|
+
item = data['item']
|
185
|
+
image_data = item['result']
|
186
|
+
output_format = item['output_format'] || 'png'
|
187
|
+
mime_type = "image/#{output_format}"
|
188
|
+
revised_prompt = item['revised_prompt']
|
189
|
+
|
190
|
+
content = build_image_content(image_data, mime_type, nil, revised_prompt)
|
191
|
+
|
192
|
+
Chunk.new(
|
193
|
+
role: :assistant,
|
194
|
+
model_id: nil,
|
195
|
+
content: content,
|
196
|
+
tool_calls: nil,
|
197
|
+
input_tokens: nil,
|
198
|
+
output_tokens: nil
|
199
|
+
)
|
200
|
+
end
|
201
|
+
|
202
|
+
def build_reasoning_chunk(data)
|
203
|
+
Chunk.new(
|
204
|
+
role: :assistant,
|
205
|
+
model_id: nil,
|
206
|
+
content: nil,
|
207
|
+
tool_calls: nil,
|
208
|
+
input_tokens: nil,
|
209
|
+
output_tokens: nil,
|
210
|
+
reasoning_id: data.dig('item', 'id')
|
211
|
+
)
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_image_content(base64_data, mime_type, model_id, revised_prompt = nil)
|
215
|
+
text_content = revised_prompt || ''
|
216
|
+
content = RubyLLM::Content.new(text_content)
|
217
|
+
content.attach(
|
218
|
+
RubyLLM::ImageAttachment.new(
|
219
|
+
data: base64_data,
|
220
|
+
mime_type: mime_type,
|
221
|
+
model_id: model_id
|
222
|
+
)
|
223
|
+
)
|
224
|
+
content
|
225
|
+
end
|
226
|
+
|
167
227
|
def create_streaming_tool_call(tool_call_data)
|
168
228
|
ToolCall.new(
|
169
229
|
id: tool_call_data['id'],
|
@@ -83,7 +83,7 @@ module RubyLLM
|
|
83
83
|
|
84
84
|
def parse_response_tool_calls(outputs)
|
85
85
|
# TODO: implement the other & built-in tools
|
86
|
-
# 'web_search_call', 'file_search_call',
|
86
|
+
# 'web_search_call', 'file_search_call',
|
87
87
|
# 'code_interpreter_call', 'local_shell_call', 'mcp_call',
|
88
88
|
# 'mcp_list_tools', 'mcp_approval_request'
|
89
89
|
outputs.select { |o| o['type'] == 'function_call' }.to_h do |o|
|
@@ -2,9 +2,7 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
module Providers
|
5
|
-
# OpenAI API integration
|
6
|
-
# function calling, and OpenAI's unique streaming format. Supports GPT-4, GPT-3.5,
|
7
|
-
# and other OpenAI models.
|
5
|
+
# OpenAI API integration.
|
8
6
|
class OpenAI < OpenAIBase
|
9
7
|
include OpenAI::Response
|
10
8
|
include OpenAI::ResponseMedia
|
@@ -26,8 +26,8 @@ module RubyLLM
|
|
26
26
|
}.compact
|
27
27
|
end
|
28
28
|
|
29
|
-
def maybe_normalize_temperature(temperature,
|
30
|
-
OpenAI::Capabilities.normalize_temperature(temperature,
|
29
|
+
def maybe_normalize_temperature(temperature, model)
|
30
|
+
OpenAI::Capabilities.normalize_temperature(temperature, model.id)
|
31
31
|
end
|
32
32
|
|
33
33
|
class << self
|
@@ -13,13 +13,11 @@ module RubyLLM
|
|
13
13
|
|
14
14
|
def parse_list_models_response(response, slug, _capabilities)
|
15
15
|
Array(response.body['data']).map do |model_data| # rubocop:disable Metrics/BlockLength
|
16
|
-
# Extract modalities directly from architecture
|
17
16
|
modalities = {
|
18
17
|
input: Array(model_data.dig('architecture', 'input_modalities')),
|
19
18
|
output: Array(model_data.dig('architecture', 'output_modalities'))
|
20
19
|
}
|
21
20
|
|
22
|
-
# Construct pricing from API data, only adding non-zero values
|
23
21
|
pricing = { text_tokens: { standard: {} } }
|
24
22
|
|
25
23
|
pricing_types = {
|
@@ -34,7 +32,6 @@ module RubyLLM
|
|
34
32
|
pricing[:text_tokens][:standard][target_key] = value * 1_000_000 if value.positive?
|
35
33
|
end
|
36
34
|
|
37
|
-
# Convert OpenRouter's supported parameters to our capability format
|
38
35
|
capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
|
39
36
|
|
40
37
|
Model::Info.new(
|
@@ -63,23 +60,11 @@ module RubyLLM
|
|
63
60
|
return [] unless params
|
64
61
|
|
65
62
|
capabilities = []
|
66
|
-
|
67
|
-
# Standard capabilities mapping
|
68
|
-
capabilities << 'streaming' # Assume all OpenRouter models support streaming
|
69
|
-
|
70
|
-
# Function calling capability
|
63
|
+
capabilities << 'streaming'
|
71
64
|
capabilities << 'function_calling' if params.include?('tools') || params.include?('tool_choice')
|
72
|
-
|
73
|
-
# Structured output capability
|
74
65
|
capabilities << 'structured_output' if params.include?('response_format')
|
75
|
-
|
76
|
-
# Batch capability
|
77
66
|
capabilities << 'batch' if params.include?('batch')
|
78
|
-
|
79
|
-
# Additional mappings based on params
|
80
|
-
# Handles advanced model capabilities that might be inferred from supported params
|
81
67
|
capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
|
82
|
-
|
83
68
|
capabilities
|
84
69
|
end
|
85
70
|
end
|
@@ -34,17 +34,13 @@ module RubyLLM
|
|
34
34
|
|
35
35
|
# If response is HTML (Perplexity returns HTML for auth errors)
|
36
36
|
if body.include?('<html>') && body.include?('<title>')
|
37
|
-
# Extract title content
|
38
37
|
title_match = body.match(%r{<title>(.+?)</title>})
|
39
38
|
if title_match
|
40
|
-
# Clean up the title - remove status code if present
|
41
39
|
message = title_match[1]
|
42
|
-
message = message.sub(/^\d+\s+/, '')
|
40
|
+
message = message.sub(/^\d+\s+/, '')
|
43
41
|
return message
|
44
42
|
end
|
45
43
|
end
|
46
|
-
|
47
|
-
# Fall back to parent's implementation
|
48
44
|
super
|
49
45
|
end
|
50
46
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Chat methods for the Vertex AI implementation
|
7
|
+
module Chat
|
8
|
+
def completion_url
|
9
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:generateContent" # rubocop:disable Layout/LineLength
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Embeddings methods for the Vertex AI implementation
|
7
|
+
module Embeddings
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def embedding_url(model:)
|
11
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{model}:predict" # rubocop:disable Layout/LineLength
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_embedding_payload(text, model:, dimensions:) # rubocop:disable Lint/UnusedMethodArgument
|
15
|
+
{
|
16
|
+
instances: [text].flatten.map { |t| { content: t.to_s } }
|
17
|
+
}.tap do |payload|
|
18
|
+
payload[:parameters] = { outputDimensionality: dimensions } if dimensions
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_embedding_response(response, model:, text:)
|
23
|
+
predictions = response.body['predictions']
|
24
|
+
vectors = predictions&.map { |p| p.dig('embeddings', 'values') }
|
25
|
+
vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
|
26
|
+
|
27
|
+
Embedding.new(vectors:, model:, input_tokens: 0)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|