ruby_llm_swarm 1.9.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -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/_content.html.erb.tt +1 -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 +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -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 +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- 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 +3 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
- data/lib/ruby_llm/active_record/acts_as.rb +174 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
- data/lib/ruby_llm/active_record/message_methods.rb +81 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +295 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +220 -0
- data/lib/ruby_llm/chat.rb +816 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +78 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +73 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +86 -0
- data/lib/ruby_llm/mime_type.rb +71 -0
- data/lib/ruby_llm/model/info.rb +111 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +33198 -0
- data/lib/ruby_llm/models.rb +231 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/moderation.rb +56 -0
- data/lib/ruby_llm/provider.rb +243 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
- data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +112 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
- data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
- data/lib/ruby_llm/providers/gemini.rb +37 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +46 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
- data/lib/ruby_llm/providers/openai/chat.rb +88 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
- data/lib/ruby_llm/providers/openai/tools.rb +98 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_responses.rb +395 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- 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/transcription.rb +16 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +35 -0
- data/lib/ruby_llm/responses_session.rb +77 -0
- data/lib/ruby_llm/stream_accumulator.rb +101 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +209 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/tool_executors.rb +125 -0
- data/lib/ruby_llm/transcription.rb +35 -0
- data/lib/ruby_llm/utils.rb +91 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +140 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +346 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Gemini
|
|
6
|
+
# Streaming methods for the Gemini API implementation
|
|
7
|
+
module Streaming
|
|
8
|
+
def stream_url
|
|
9
|
+
"models/#{@model}:streamGenerateContent?alt=sse"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_chunk(data)
|
|
13
|
+
Chunk.new(
|
|
14
|
+
role: :assistant,
|
|
15
|
+
model_id: extract_model_id(data),
|
|
16
|
+
content: extract_content(data),
|
|
17
|
+
input_tokens: extract_input_tokens(data),
|
|
18
|
+
output_tokens: extract_output_tokens(data),
|
|
19
|
+
tool_calls: extract_tool_calls(data)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def extract_model_id(data)
|
|
26
|
+
data['modelVersion']
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def extract_content(data)
|
|
30
|
+
return nil unless data['candidates']&.any?
|
|
31
|
+
|
|
32
|
+
candidate = data['candidates'][0]
|
|
33
|
+
parts = candidate.dig('content', 'parts')
|
|
34
|
+
return nil unless parts
|
|
35
|
+
|
|
36
|
+
text_parts = parts.select { |p| p['text'] }
|
|
37
|
+
text_parts.map { |p| p['text'] }.join if text_parts.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def extract_input_tokens(data)
|
|
41
|
+
data.dig('usageMetadata', 'promptTokenCount')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_output_tokens(data)
|
|
45
|
+
candidates = data.dig('usageMetadata', 'candidatesTokenCount') || 0
|
|
46
|
+
thoughts = data.dig('usageMetadata', 'thoughtsTokenCount') || 0
|
|
47
|
+
total = candidates + thoughts
|
|
48
|
+
total.positive? ? total : nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_streaming_error(data)
|
|
52
|
+
error_data = JSON.parse(data)
|
|
53
|
+
[error_data['error']['code'], error_data['error']['message']]
|
|
54
|
+
rescue JSON::ParserError => e
|
|
55
|
+
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
|
56
|
+
[500, "Failed to parse error: #{data}"]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Gemini
|
|
6
|
+
# Tools methods for the Gemini API implementation
|
|
7
|
+
module Tools
|
|
8
|
+
def format_tools(tools)
|
|
9
|
+
return [] if tools.empty?
|
|
10
|
+
|
|
11
|
+
[{
|
|
12
|
+
functionDeclarations: tools.values.map { |tool| function_declaration_for(tool) }
|
|
13
|
+
}]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def format_tool_call(msg)
|
|
17
|
+
parts = []
|
|
18
|
+
|
|
19
|
+
if msg.content && !(msg.content.respond_to?(:empty?) && msg.content.empty?)
|
|
20
|
+
formatted_content = Media.format_content(msg.content)
|
|
21
|
+
parts.concat(formatted_content.is_a?(Array) ? formatted_content : [formatted_content])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
msg.tool_calls.each_value do |tool_call|
|
|
25
|
+
parts << {
|
|
26
|
+
functionCall: {
|
|
27
|
+
name: tool_call.name,
|
|
28
|
+
args: tool_call.arguments
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
parts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format_tool_result(msg, function_name = nil)
|
|
37
|
+
function_name ||= msg.tool_call_id
|
|
38
|
+
|
|
39
|
+
[{
|
|
40
|
+
functionResponse: {
|
|
41
|
+
name: function_name,
|
|
42
|
+
response: {
|
|
43
|
+
name: function_name,
|
|
44
|
+
content: Media.format_content(msg.content)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def extract_tool_calls(data) # rubocop:disable Metrics/PerceivedComplexity
|
|
51
|
+
return nil unless data
|
|
52
|
+
|
|
53
|
+
candidate = data.is_a?(Hash) ? data.dig('candidates', 0) : nil
|
|
54
|
+
return nil unless candidate
|
|
55
|
+
|
|
56
|
+
parts = candidate.dig('content', 'parts')
|
|
57
|
+
return nil unless parts.is_a?(Array)
|
|
58
|
+
|
|
59
|
+
tool_calls = parts.each_with_object({}) do |part, result|
|
|
60
|
+
function_data = part['functionCall']
|
|
61
|
+
next unless function_data
|
|
62
|
+
|
|
63
|
+
id = SecureRandom.uuid
|
|
64
|
+
|
|
65
|
+
result[id] = ToolCall.new(
|
|
66
|
+
id:,
|
|
67
|
+
name: function_data['name'],
|
|
68
|
+
arguments: function_data['args'] || {}
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
tool_calls.empty? ? nil : tool_calls
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def function_declaration_for(tool)
|
|
78
|
+
parameters_schema = tool.params_schema ||
|
|
79
|
+
RubyLLM::Tool::SchemaDefinition.from_parameters(tool.parameters)&.json_schema
|
|
80
|
+
|
|
81
|
+
declaration = {
|
|
82
|
+
name: tool.name,
|
|
83
|
+
description: tool.description
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declaration[:parameters] = convert_tool_schema_to_gemini(parameters_schema) if parameters_schema
|
|
87
|
+
|
|
88
|
+
return declaration if tool.provider_params.empty?
|
|
89
|
+
|
|
90
|
+
RubyLLM::Utils.deep_merge(declaration, tool.provider_params)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def convert_tool_schema_to_gemini(schema)
|
|
94
|
+
return nil unless schema
|
|
95
|
+
|
|
96
|
+
schema = RubyLLM::Utils.deep_stringify_keys(schema)
|
|
97
|
+
|
|
98
|
+
raise ArgumentError, 'Gemini tool parameters must be objects' unless schema['type'] == 'object'
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
type: 'OBJECT',
|
|
102
|
+
properties: schema.fetch('properties', {}).transform_values { |property| convert_property(property) },
|
|
103
|
+
required: (schema['required'] || []).map(&:to_s)
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def convert_property(property_schema) # rubocop:disable Metrics/PerceivedComplexity
|
|
108
|
+
normalized_schema = normalize_any_of_schema(property_schema)
|
|
109
|
+
working_schema = normalized_schema || property_schema
|
|
110
|
+
|
|
111
|
+
type = param_type_for_gemini(working_schema['type'])
|
|
112
|
+
|
|
113
|
+
property = {
|
|
114
|
+
type: type
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
copy_common_attributes(property, property_schema)
|
|
118
|
+
copy_common_attributes(property, working_schema)
|
|
119
|
+
|
|
120
|
+
case type
|
|
121
|
+
when 'ARRAY'
|
|
122
|
+
items_schema = working_schema['items'] || property_schema['items'] || { 'type' => 'string' }
|
|
123
|
+
property[:items] = convert_property(items_schema)
|
|
124
|
+
copy_tool_attributes(property, working_schema, %w[minItems maxItems])
|
|
125
|
+
copy_tool_attributes(property, property_schema, %w[minItems maxItems])
|
|
126
|
+
when 'OBJECT'
|
|
127
|
+
nested_properties = working_schema.fetch('properties', {}).transform_values do |child|
|
|
128
|
+
convert_property(child)
|
|
129
|
+
end
|
|
130
|
+
property[:properties] = nested_properties
|
|
131
|
+
required = working_schema['required'] || property_schema['required']
|
|
132
|
+
property[:required] = required.map(&:to_s) if required
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
property
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def copy_common_attributes(target, source)
|
|
139
|
+
copy_tool_attributes(target, source, %w[description enum format nullable maximum minimum multipleOf])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def copy_tool_attributes(target, source, attributes)
|
|
143
|
+
attributes.each do |attribute|
|
|
144
|
+
value = schema_value(source, attribute)
|
|
145
|
+
next if value.nil?
|
|
146
|
+
|
|
147
|
+
target[attribute.to_sym] = value
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def normalize_any_of_schema(schema) # rubocop:disable Metrics/PerceivedComplexity
|
|
152
|
+
any_of = schema['anyOf'] || schema[:anyOf]
|
|
153
|
+
return nil unless any_of.is_a?(Array) && any_of.any?
|
|
154
|
+
|
|
155
|
+
null_entries, non_null_entries = any_of.partition { |entry| schema_type(entry).to_s == 'null' }
|
|
156
|
+
|
|
157
|
+
if non_null_entries.size == 1 && null_entries.any?
|
|
158
|
+
normalized = RubyLLM::Utils.deep_dup(non_null_entries.first)
|
|
159
|
+
normalized['nullable'] = true
|
|
160
|
+
normalized
|
|
161
|
+
elsif non_null_entries.any?
|
|
162
|
+
RubyLLM::Utils.deep_dup(non_null_entries.first)
|
|
163
|
+
else
|
|
164
|
+
{ 'type' => 'string', 'nullable' => true }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def schema_type(schema)
|
|
169
|
+
schema['type'] || schema[:type]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def schema_value(source, attribute) # rubocop:disable Metrics/PerceivedComplexity
|
|
173
|
+
case attribute
|
|
174
|
+
when 'multipleOf'
|
|
175
|
+
source['multipleOf'] || source[:multipleOf] || source['multiple_of'] || source[:multiple_of]
|
|
176
|
+
when 'minItems'
|
|
177
|
+
source['minItems'] || source[:minItems] || source['min_items'] || source[:min_items]
|
|
178
|
+
when 'maxItems'
|
|
179
|
+
source['maxItems'] || source[:maxItems] || source['max_items'] || source[:max_items]
|
|
180
|
+
else
|
|
181
|
+
source[attribute] || source[attribute.to_sym]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def param_type_for_gemini(type)
|
|
186
|
+
case type.to_s.downcase
|
|
187
|
+
when 'integer' then 'INTEGER'
|
|
188
|
+
when 'number', 'float', 'double' then 'NUMBER'
|
|
189
|
+
when 'boolean' then 'BOOLEAN'
|
|
190
|
+
when 'array' then 'ARRAY'
|
|
191
|
+
when 'object' then 'OBJECT'
|
|
192
|
+
else 'STRING'
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Gemini
|
|
6
|
+
# Audio transcription helpers for the Gemini API implementation
|
|
7
|
+
module Transcription
|
|
8
|
+
DEFAULT_PROMPT = 'Transcribe the provided audio and respond with only the transcript text.'
|
|
9
|
+
|
|
10
|
+
def transcribe(audio_file, model:, language:, **options)
|
|
11
|
+
attachment = Attachment.new(audio_file)
|
|
12
|
+
payload = render_transcription_payload(attachment, language:, **options)
|
|
13
|
+
response = @connection.post(transcription_url(model), payload)
|
|
14
|
+
parse_transcription_response(response, model:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def transcription_url(model)
|
|
20
|
+
"models/#{model}:generateContent"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_transcription_payload(attachment, language:, **options)
|
|
24
|
+
prompt = build_prompt(options[:prompt], language)
|
|
25
|
+
audio_part = format_audio_part(attachment)
|
|
26
|
+
|
|
27
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.audio?
|
|
28
|
+
|
|
29
|
+
payload = {
|
|
30
|
+
contents: [
|
|
31
|
+
{
|
|
32
|
+
role: 'user',
|
|
33
|
+
parts: [
|
|
34
|
+
{ text: prompt },
|
|
35
|
+
audio_part
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
generation_config = build_generation_config(options)
|
|
42
|
+
payload[:generationConfig] = generation_config unless generation_config.empty?
|
|
43
|
+
payload[:safetySettings] = options[:safety_settings] if options[:safety_settings]
|
|
44
|
+
|
|
45
|
+
payload
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_generation_config(options)
|
|
49
|
+
config = {}
|
|
50
|
+
response_mime_type = options.fetch(:response_mime_type, 'text/plain')
|
|
51
|
+
|
|
52
|
+
config[:responseMimeType] = response_mime_type if response_mime_type
|
|
53
|
+
config[:temperature] = options[:temperature] if options.key?(:temperature)
|
|
54
|
+
config[:maxOutputTokens] = options[:max_output_tokens] if options[:max_output_tokens]
|
|
55
|
+
|
|
56
|
+
config
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_prompt(custom_prompt, language)
|
|
60
|
+
prompt = DEFAULT_PROMPT
|
|
61
|
+
prompt += " Respond in the #{language} language." if language
|
|
62
|
+
prompt += " #{custom_prompt}" if custom_prompt
|
|
63
|
+
prompt
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_audio_part(attachment)
|
|
67
|
+
{
|
|
68
|
+
inline_data: {
|
|
69
|
+
mime_type: attachment.mime_type,
|
|
70
|
+
data: attachment.encoded
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_transcription_response(response, model:)
|
|
76
|
+
data = response.body
|
|
77
|
+
text = extract_text(data)
|
|
78
|
+
|
|
79
|
+
usage = extract_usage(data)
|
|
80
|
+
|
|
81
|
+
RubyLLM::Transcription.new(
|
|
82
|
+
text: text,
|
|
83
|
+
model: model,
|
|
84
|
+
input_tokens: usage[:input_tokens],
|
|
85
|
+
output_tokens: usage[:output_tokens]
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extract_text(data)
|
|
90
|
+
candidate = data.is_a?(Hash) ? data.dig('candidates', 0) : nil
|
|
91
|
+
return unless candidate
|
|
92
|
+
|
|
93
|
+
parts = candidate.dig('content', 'parts') || []
|
|
94
|
+
texts = parts.filter_map { |part| part['text'] }
|
|
95
|
+
texts.join if texts.any?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract_usage(data)
|
|
99
|
+
metadata = data.is_a?(Hash) ? data['usageMetadata'] : nil
|
|
100
|
+
return { input_tokens: nil, output_tokens: nil } unless metadata
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
input_tokens: metadata['promptTokenCount'],
|
|
104
|
+
output_tokens: sum_output_tokens(metadata)
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def sum_output_tokens(metadata)
|
|
109
|
+
candidates = metadata['candidatesTokenCount'] || 0
|
|
110
|
+
thoughts = metadata['thoughtsTokenCount'] || 0
|
|
111
|
+
candidates + thoughts
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Native Gemini API implementation
|
|
6
|
+
class Gemini < Provider
|
|
7
|
+
include Gemini::Chat
|
|
8
|
+
include Gemini::Embeddings
|
|
9
|
+
include Gemini::Images
|
|
10
|
+
include Gemini::Models
|
|
11
|
+
include Gemini::Transcription
|
|
12
|
+
include Gemini::Streaming
|
|
13
|
+
include Gemini::Tools
|
|
14
|
+
include Gemini::Media
|
|
15
|
+
|
|
16
|
+
def api_base
|
|
17
|
+
@config.gemini_api_base || 'https://generativelanguage.googleapis.com/v1beta'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def headers
|
|
21
|
+
{
|
|
22
|
+
'x-goog-api-key' => @config.gemini_api_key
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def capabilities
|
|
28
|
+
Gemini::Capabilities
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def configuration_requirements
|
|
32
|
+
%i[gemini_api_key]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class GPUStack
|
|
6
|
+
# Chat methods of the GPUStack API integration
|
|
7
|
+
module Chat
|
|
8
|
+
module_function
|
|
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
|
+
|
|
21
|
+
def format_role(role)
|
|
22
|
+
role.to_s
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
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.value if content.is_a?(RubyLLM::Content::Raw)
|
|
14
|
+
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
|
15
|
+
return content unless content.is_a?(Content)
|
|
16
|
+
|
|
17
|
+
parts = []
|
|
18
|
+
parts << format_text(content.text) if content.text
|
|
19
|
+
|
|
20
|
+
content.attachments.each do |attachment|
|
|
21
|
+
case attachment.type
|
|
22
|
+
when :image
|
|
23
|
+
parts << GPUStack::Media.format_image(attachment)
|
|
24
|
+
when :text
|
|
25
|
+
parts << format_text_file(attachment)
|
|
26
|
+
else
|
|
27
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
parts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def format_image(image)
|
|
35
|
+
{
|
|
36
|
+
type: 'image_url',
|
|
37
|
+
image_url: {
|
|
38
|
+
url: image.for_llm,
|
|
39
|
+
detail: 'auto'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class GPUStack
|
|
6
|
+
# Models methods of the GPUStack API integration
|
|
7
|
+
module Models
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def models_url
|
|
11
|
+
'models'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_list_models_response(response, slug, _capabilities)
|
|
15
|
+
items = response.body['items'] || []
|
|
16
|
+
items.map do |model|
|
|
17
|
+
Model::Info.new(
|
|
18
|
+
id: model['name'],
|
|
19
|
+
name: model['name'],
|
|
20
|
+
created_at: model['created_at'] ? Time.parse(model['created_at']) : nil,
|
|
21
|
+
provider: slug,
|
|
22
|
+
family: 'gpustack',
|
|
23
|
+
metadata: {
|
|
24
|
+
description: model['description'],
|
|
25
|
+
source: model['source'],
|
|
26
|
+
huggingface_repo_id: model['huggingface_repo_id'],
|
|
27
|
+
ollama_library_model_name: model['ollama_library_model_name'],
|
|
28
|
+
backend: model['backend'],
|
|
29
|
+
meta: model['meta'],
|
|
30
|
+
categories: model['categories']
|
|
31
|
+
},
|
|
32
|
+
context_window: model.dig('meta', 'n_ctx'),
|
|
33
|
+
max_output_tokens: model.dig('meta', 'n_ctx'),
|
|
34
|
+
capabilities: build_capabilities(model),
|
|
35
|
+
modalities: build_modalities(model),
|
|
36
|
+
pricing: {}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def determine_model_type(model)
|
|
44
|
+
return 'embedding' if model['categories']&.include?('embedding')
|
|
45
|
+
return 'chat' if model['categories']&.include?('llm')
|
|
46
|
+
|
|
47
|
+
'other'
|
|
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
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# GPUStack API integration based on Ollama.
|
|
6
|
+
class GPUStack < OpenAI
|
|
7
|
+
include GPUStack::Chat
|
|
8
|
+
include GPUStack::Models
|
|
9
|
+
include GPUStack::Media
|
|
10
|
+
|
|
11
|
+
def api_base
|
|
12
|
+
@config.gpustack_api_base
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def headers
|
|
16
|
+
return {} unless @config.gpustack_api_key
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
'Authorization' => "Bearer #{@config.gpustack_api_key}"
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def local?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configuration_requirements
|
|
29
|
+
%i[gpustack_api_base]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|