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,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Perplexity
|
|
6
|
+
# Determines capabilities and pricing for Perplexity models
|
|
7
|
+
module Capabilities
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def context_window_for(model_id)
|
|
11
|
+
case model_id
|
|
12
|
+
when /sonar-pro/ then 200_000
|
|
13
|
+
else 128_000
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def max_tokens_for(model_id)
|
|
18
|
+
case model_id
|
|
19
|
+
when /sonar-(?:pro|reasoning-pro)/ then 8_192
|
|
20
|
+
else 4_096
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def input_price_for(model_id)
|
|
25
|
+
PRICES.dig(model_family(model_id), :input) || 1.0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def output_price_for(model_id)
|
|
29
|
+
PRICES.dig(model_family(model_id), :output) || 1.0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def supports_vision?(model_id)
|
|
33
|
+
case model_id
|
|
34
|
+
when /sonar-reasoning-pro/, /sonar-reasoning/, /sonar-pro/, /sonar/ then true
|
|
35
|
+
else false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def supports_functions?(_model_id)
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def supports_json_mode?(_model_id)
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_display_name(model_id)
|
|
48
|
+
case model_id
|
|
49
|
+
when 'sonar' then 'Sonar'
|
|
50
|
+
when 'sonar-pro' then 'Sonar Pro'
|
|
51
|
+
when 'sonar-reasoning' then 'Sonar Reasoning'
|
|
52
|
+
when 'sonar-reasoning-pro' then 'Sonar Reasoning Pro'
|
|
53
|
+
when 'sonar-deep-research' then 'Sonar Deep Research'
|
|
54
|
+
else
|
|
55
|
+
model_id.split('-')
|
|
56
|
+
.map(&:capitalize)
|
|
57
|
+
.join(' ')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def model_type(_model_id)
|
|
62
|
+
'chat'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def model_family(model_id)
|
|
66
|
+
case model_id
|
|
67
|
+
when 'sonar' then :sonar
|
|
68
|
+
when 'sonar-pro' then :sonar_pro
|
|
69
|
+
when 'sonar-reasoning' then :sonar_reasoning
|
|
70
|
+
when 'sonar-reasoning-pro' then :sonar_reasoning_pro
|
|
71
|
+
when 'sonar-deep-research' then :sonar_deep_research
|
|
72
|
+
else :unknown
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def modalities_for(_model_id)
|
|
77
|
+
{
|
|
78
|
+
input: ['text'],
|
|
79
|
+
output: ['text']
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def capabilities_for(model_id)
|
|
84
|
+
capabilities = %w[streaming json_mode]
|
|
85
|
+
capabilities << 'vision' if supports_vision?(model_id)
|
|
86
|
+
capabilities
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def pricing_for(model_id)
|
|
90
|
+
family = model_family(model_id)
|
|
91
|
+
prices = PRICES.fetch(family, { input: 1.0, output: 1.0 })
|
|
92
|
+
|
|
93
|
+
standard_pricing = {
|
|
94
|
+
input_per_million: prices[:input],
|
|
95
|
+
output_per_million: prices[:output]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
standard_pricing[:citation_per_million] = prices[:citation] if prices[:citation]
|
|
99
|
+
standard_pricing[:reasoning_per_million] = prices[:reasoning] if prices[:reasoning]
|
|
100
|
+
standard_pricing[:search_per_thousand] = prices[:search_queries] if prices[:search_queries]
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
text_tokens: {
|
|
104
|
+
standard: standard_pricing
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
PRICES = {
|
|
110
|
+
sonar: {
|
|
111
|
+
input: 1.0,
|
|
112
|
+
output: 1.0
|
|
113
|
+
},
|
|
114
|
+
sonar_pro: {
|
|
115
|
+
input: 3.0,
|
|
116
|
+
output: 15.0
|
|
117
|
+
},
|
|
118
|
+
sonar_reasoning: {
|
|
119
|
+
input: 1.0,
|
|
120
|
+
output: 5.0
|
|
121
|
+
},
|
|
122
|
+
sonar_reasoning_pro: {
|
|
123
|
+
input: 2.0,
|
|
124
|
+
output: 8.0
|
|
125
|
+
},
|
|
126
|
+
sonar_deep_research: {
|
|
127
|
+
input: 2.0,
|
|
128
|
+
output: 8.0,
|
|
129
|
+
citation: 2.0,
|
|
130
|
+
reasoning: 3.0,
|
|
131
|
+
search_queries: 5.0
|
|
132
|
+
}
|
|
133
|
+
}.freeze
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Perplexity
|
|
6
|
+
# Models methods of the Perplexity API integration
|
|
7
|
+
module Models
|
|
8
|
+
def list_models(**)
|
|
9
|
+
slug = 'perplexity'
|
|
10
|
+
capabilities = Perplexity::Capabilities
|
|
11
|
+
parse_list_models_response(nil, slug, capabilities)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_list_models_response(_response, slug, capabilities)
|
|
15
|
+
[
|
|
16
|
+
create_model_info('sonar', slug, capabilities),
|
|
17
|
+
create_model_info('sonar-pro', slug, capabilities),
|
|
18
|
+
create_model_info('sonar-reasoning', slug, capabilities),
|
|
19
|
+
create_model_info('sonar-reasoning-pro', slug, capabilities),
|
|
20
|
+
create_model_info('sonar-deep-research', slug, capabilities)
|
|
21
|
+
]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_model_info(id, slug, capabilities)
|
|
25
|
+
Model::Info.new(
|
|
26
|
+
id: id,
|
|
27
|
+
name: capabilities.format_display_name(id),
|
|
28
|
+
provider: slug,
|
|
29
|
+
family: capabilities.model_family(id).to_s,
|
|
30
|
+
created_at: Time.now,
|
|
31
|
+
context_window: capabilities.context_window_for(id),
|
|
32
|
+
max_output_tokens: capabilities.max_tokens_for(id),
|
|
33
|
+
modalities: capabilities.modalities_for(id),
|
|
34
|
+
capabilities: capabilities.capabilities_for(id),
|
|
35
|
+
pricing: capabilities.pricing_for(id),
|
|
36
|
+
metadata: {}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Perplexity API integration.
|
|
6
|
+
class Perplexity < OpenAI
|
|
7
|
+
include Perplexity::Chat
|
|
8
|
+
include Perplexity::Models
|
|
9
|
+
|
|
10
|
+
def api_base
|
|
11
|
+
'https://api.perplexity.ai'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def headers
|
|
15
|
+
{
|
|
16
|
+
'Authorization' => "Bearer #{@config.perplexity_api_key}",
|
|
17
|
+
'Content-Type' => 'application/json'
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def capabilities
|
|
23
|
+
Perplexity::Capabilities
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configuration_requirements
|
|
27
|
+
%i[perplexity_api_key]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_error(response)
|
|
32
|
+
body = response.body
|
|
33
|
+
return if body.empty?
|
|
34
|
+
|
|
35
|
+
# If response is HTML (Perplexity returns HTML for auth errors)
|
|
36
|
+
if body.include?('<html>') && body.include?('<title>')
|
|
37
|
+
title_match = body.match(%r{<title>(.+?)</title>})
|
|
38
|
+
if title_match
|
|
39
|
+
message = title_match[1]
|
|
40
|
+
message = message.sub(/^\d+\s+/, '')
|
|
41
|
+
return message
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
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
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class VertexAI
|
|
6
|
+
# Models methods for the Vertex AI integration
|
|
7
|
+
module Models
|
|
8
|
+
# Gemini and other Google models that aren't returned by the API
|
|
9
|
+
KNOWN_GOOGLE_MODELS = %w[
|
|
10
|
+
gemini-2.5-flash-lite
|
|
11
|
+
gemini-2.5-pro
|
|
12
|
+
gemini-2.5-flash
|
|
13
|
+
gemini-2.0-flash-lite-001
|
|
14
|
+
gemini-2.0-flash-001
|
|
15
|
+
gemini-2.0-flash
|
|
16
|
+
gemini-2.0-flash-exp
|
|
17
|
+
gemini-1.5-pro-002
|
|
18
|
+
gemini-1.5-pro
|
|
19
|
+
gemini-1.5-flash-002
|
|
20
|
+
gemini-1.5-flash
|
|
21
|
+
gemini-1.5-flash-8b
|
|
22
|
+
gemini-pro
|
|
23
|
+
gemini-pro-vision
|
|
24
|
+
gemini-exp-1206
|
|
25
|
+
gemini-exp-1121
|
|
26
|
+
gemini-embedding-001
|
|
27
|
+
text-embedding-005
|
|
28
|
+
text-embedding-004
|
|
29
|
+
text-multilingual-embedding-002
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def list_models
|
|
33
|
+
all_models = []
|
|
34
|
+
page_token = nil
|
|
35
|
+
|
|
36
|
+
all_models.concat(build_known_models)
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
response = @connection.get('publishers/google/models') do |req|
|
|
40
|
+
req.headers['x-goog-user-project'] = @config.vertexai_project_id
|
|
41
|
+
req.params = { pageSize: 100 }
|
|
42
|
+
req.params[:pageToken] = page_token if page_token
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
publisher_models = response.body['publisherModels'] || []
|
|
46
|
+
publisher_models.each do |model_data|
|
|
47
|
+
next if model_data['launchStage'] == 'DEPRECATED'
|
|
48
|
+
|
|
49
|
+
model_id = extract_model_id_from_path(model_data['name'])
|
|
50
|
+
all_models << build_model_from_api_data(model_data, model_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
page_token = response.body['nextPageToken']
|
|
54
|
+
break unless page_token
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
all_models
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
RubyLLM.logger.debug "Error fetching Vertex AI models: #{e.message}"
|
|
60
|
+
build_known_models
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_known_models
|
|
66
|
+
KNOWN_GOOGLE_MODELS.map do |model_id|
|
|
67
|
+
Model::Info.new(
|
|
68
|
+
id: model_id,
|
|
69
|
+
name: model_id,
|
|
70
|
+
provider: slug,
|
|
71
|
+
family: determine_model_family(model_id),
|
|
72
|
+
created_at: nil,
|
|
73
|
+
context_window: nil,
|
|
74
|
+
max_output_tokens: nil,
|
|
75
|
+
modalities: nil,
|
|
76
|
+
capabilities: %w[streaming function_calling],
|
|
77
|
+
pricing: nil,
|
|
78
|
+
metadata: {
|
|
79
|
+
source: 'known_models'
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_model_from_api_data(model_data, model_id)
|
|
86
|
+
Model::Info.new(
|
|
87
|
+
id: model_id,
|
|
88
|
+
name: model_id,
|
|
89
|
+
provider: slug,
|
|
90
|
+
family: determine_model_family(model_id),
|
|
91
|
+
created_at: nil,
|
|
92
|
+
context_window: nil,
|
|
93
|
+
max_output_tokens: nil,
|
|
94
|
+
modalities: nil,
|
|
95
|
+
capabilities: extract_capabilities(model_data),
|
|
96
|
+
pricing: nil,
|
|
97
|
+
metadata: {
|
|
98
|
+
version_id: model_data['versionId'],
|
|
99
|
+
open_source_category: model_data['openSourceCategory'],
|
|
100
|
+
launch_stage: model_data['launchStage'],
|
|
101
|
+
supported_actions: model_data['supportedActions'],
|
|
102
|
+
publisher_model_template: model_data['publisherModelTemplate']
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_model_id_from_path(path)
|
|
108
|
+
path.split('/').last
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def determine_model_family(model_id)
|
|
112
|
+
case model_id
|
|
113
|
+
when /^gemini-2\.\d+/ then 'gemini-2'
|
|
114
|
+
when /^gemini-1\.\d+/ then 'gemini-1.5'
|
|
115
|
+
when /^text-embedding/ then 'text-embedding'
|
|
116
|
+
when /bison/ then 'palm'
|
|
117
|
+
else 'gemini'
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_capabilities(model_data)
|
|
122
|
+
capabilities = ['streaming']
|
|
123
|
+
model_name = model_data['name']
|
|
124
|
+
capabilities << 'function_calling' if model_name.include?('gemini')
|
|
125
|
+
capabilities.uniq
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class VertexAI
|
|
6
|
+
# Streaming methods for the Vertex AI implementation
|
|
7
|
+
module Streaming
|
|
8
|
+
def stream_url
|
|
9
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:streamGenerateContent?alt=sse" # rubocop:disable Layout/LineLength
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class VertexAI
|
|
6
|
+
# Vertex AI specific helpers for audio transcription
|
|
7
|
+
module Transcription
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def transcription_url(model)
|
|
11
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{model}:generateContent" # rubocop:disable Layout/LineLength
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Google Vertex AI implementation
|
|
6
|
+
class VertexAI < Gemini
|
|
7
|
+
include VertexAI::Chat
|
|
8
|
+
include VertexAI::Streaming
|
|
9
|
+
include VertexAI::Embeddings
|
|
10
|
+
include VertexAI::Models
|
|
11
|
+
include VertexAI::Transcription
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
super
|
|
15
|
+
@authorizer = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def api_base
|
|
19
|
+
"https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def headers
|
|
23
|
+
if defined?(VCR) && !VCR.current_cassette.recording?
|
|
24
|
+
{ 'Authorization' => 'Bearer test-token' }
|
|
25
|
+
else
|
|
26
|
+
initialize_authorizer unless @authorizer
|
|
27
|
+
@authorizer.apply({})
|
|
28
|
+
end
|
|
29
|
+
rescue Google::Auth::AuthorizationError => e
|
|
30
|
+
raise UnauthorizedError.new(nil, "Invalid Google Cloud credentials for Vertex AI: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def configuration_requirements
|
|
35
|
+
%i[vertexai_project_id vertexai_location]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def initialize_authorizer
|
|
42
|
+
require 'googleauth'
|
|
43
|
+
@authorizer = ::Google::Auth.get_application_default(
|
|
44
|
+
scope: [
|
|
45
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
46
|
+
'https://www.googleapis.com/auth/generative-language.retriever'
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
rescue LoadError
|
|
50
|
+
raise Error,
|
|
51
|
+
'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if defined?(Rails::Railtie)
|
|
4
|
+
module RubyLLM
|
|
5
|
+
# Rails integration for RubyLLM
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer 'ruby_llm.inflections' do
|
|
8
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
9
|
+
inflect.acronym 'RubyLLM'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer 'ruby_llm.active_record' do
|
|
14
|
+
ActiveSupport.on_load :active_record do
|
|
15
|
+
if RubyLLM.config.use_new_acts_as
|
|
16
|
+
require 'ruby_llm/active_record/acts_as'
|
|
17
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
|
|
18
|
+
else
|
|
19
|
+
require 'ruby_llm/active_record/acts_as_legacy'
|
|
20
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy
|
|
21
|
+
|
|
22
|
+
Rails.logger.warn(
|
|
23
|
+
"\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \
|
|
24
|
+
"Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
rake_tasks do
|
|
31
|
+
load 'tasks/ruby_llm.rake'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Manages state for OpenAI Responses API stateful conversations.
|
|
5
|
+
# Tracks response IDs, session validity, and failure recovery.
|
|
6
|
+
class ResponsesSession
|
|
7
|
+
RESPONSE_ID_TTL = 300 # 5 minutes
|
|
8
|
+
MAX_FAILURES = 2
|
|
9
|
+
|
|
10
|
+
attr_reader :response_id, :last_activity, :failure_count
|
|
11
|
+
|
|
12
|
+
def initialize(response_id: nil, last_activity: nil, failure_count: 0, disabled: false)
|
|
13
|
+
@response_id = response_id
|
|
14
|
+
@last_activity = last_activity
|
|
15
|
+
@failure_count = failure_count
|
|
16
|
+
@disabled = disabled
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset!
|
|
20
|
+
@response_id = nil
|
|
21
|
+
@last_activity = nil
|
|
22
|
+
@failure_count = 0
|
|
23
|
+
@disabled = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update(new_response_id)
|
|
27
|
+
@response_id = new_response_id
|
|
28
|
+
@last_activity = Time.now
|
|
29
|
+
@failure_count = 0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def valid?
|
|
33
|
+
return false if @disabled
|
|
34
|
+
return false unless @response_id
|
|
35
|
+
return false unless @last_activity
|
|
36
|
+
|
|
37
|
+
(Time.now - @last_activity) < RESPONSE_ID_TTL
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def record_failure!
|
|
41
|
+
@failure_count += 1
|
|
42
|
+
|
|
43
|
+
if @failure_count >= MAX_FAILURES
|
|
44
|
+
@disabled = true
|
|
45
|
+
else
|
|
46
|
+
# Reset response_id and last_activity but preserve failure_count
|
|
47
|
+
@response_id = nil
|
|
48
|
+
@last_activity = nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def disabled?
|
|
53
|
+
@disabled
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
response_id: @response_id,
|
|
59
|
+
last_activity: @last_activity&.iso8601,
|
|
60
|
+
failure_count: @failure_count,
|
|
61
|
+
disabled: @disabled
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.from_h(hash)
|
|
66
|
+
hash = hash.transform_keys(&:to_sym)
|
|
67
|
+
last_activity = hash[:last_activity] ? Time.parse(hash[:last_activity]) : nil
|
|
68
|
+
|
|
69
|
+
new(
|
|
70
|
+
response_id: hash[:response_id],
|
|
71
|
+
last_activity: last_activity,
|
|
72
|
+
failure_count: hash[:failure_count] || 0,
|
|
73
|
+
disabled: hash[:disabled] || false
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|