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.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +175 -0
  4. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  22. data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
  23. data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
  24. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  25. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  26. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
  27. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  28. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
  29. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
  30. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
  31. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  32. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  33. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  34. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  35. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
  36. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  37. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  38. data/lib/ruby_llm/active_record/acts_as.rb +174 -0
  39. data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
  40. data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
  41. data/lib/ruby_llm/active_record/message_methods.rb +81 -0
  42. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  43. data/lib/ruby_llm/aliases.json +295 -0
  44. data/lib/ruby_llm/aliases.rb +38 -0
  45. data/lib/ruby_llm/attachment.rb +220 -0
  46. data/lib/ruby_llm/chat.rb +816 -0
  47. data/lib/ruby_llm/chunk.rb +6 -0
  48. data/lib/ruby_llm/configuration.rb +78 -0
  49. data/lib/ruby_llm/connection.rb +126 -0
  50. data/lib/ruby_llm/content.rb +73 -0
  51. data/lib/ruby_llm/context.rb +29 -0
  52. data/lib/ruby_llm/embedding.rb +29 -0
  53. data/lib/ruby_llm/error.rb +84 -0
  54. data/lib/ruby_llm/image.rb +49 -0
  55. data/lib/ruby_llm/message.rb +86 -0
  56. data/lib/ruby_llm/mime_type.rb +71 -0
  57. data/lib/ruby_llm/model/info.rb +111 -0
  58. data/lib/ruby_llm/model/modalities.rb +22 -0
  59. data/lib/ruby_llm/model/pricing.rb +48 -0
  60. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  61. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  62. data/lib/ruby_llm/model.rb +7 -0
  63. data/lib/ruby_llm/models.json +33198 -0
  64. data/lib/ruby_llm/models.rb +231 -0
  65. data/lib/ruby_llm/models_schema.json +168 -0
  66. data/lib/ruby_llm/moderation.rb +56 -0
  67. data/lib/ruby_llm/provider.rb +243 -0
  68. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  69. data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
  70. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  71. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  72. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  73. data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
  74. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  75. data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
  76. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  77. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  78. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  79. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  80. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  81. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  82. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  83. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  84. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  85. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
  86. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  87. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  88. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  89. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  90. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  91. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  92. data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
  93. data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
  94. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  95. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  96. data/lib/ruby_llm/providers/gemini/media.rb +112 -0
  97. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  98. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  99. data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
  100. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  101. data/lib/ruby_llm/providers/gemini.rb +37 -0
  102. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  103. data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
  104. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  105. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  106. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  107. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  108. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  109. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  110. data/lib/ruby_llm/providers/mistral.rb +32 -0
  111. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  112. data/lib/ruby_llm/providers/ollama/media.rb +46 -0
  113. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  114. data/lib/ruby_llm/providers/ollama.rb +30 -0
  115. data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
  116. data/lib/ruby_llm/providers/openai/chat.rb +88 -0
  117. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  118. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  119. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  120. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  121. data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
  122. data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
  123. data/lib/ruby_llm/providers/openai/tools.rb +98 -0
  124. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  125. data/lib/ruby_llm/providers/openai.rb +44 -0
  126. data/lib/ruby_llm/providers/openai_responses.rb +395 -0
  127. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  128. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  129. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  130. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  131. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  132. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  133. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  134. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  135. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  136. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  137. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  138. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  139. data/lib/ruby_llm/railtie.rb +35 -0
  140. data/lib/ruby_llm/responses_session.rb +77 -0
  141. data/lib/ruby_llm/stream_accumulator.rb +101 -0
  142. data/lib/ruby_llm/streaming.rb +153 -0
  143. data/lib/ruby_llm/tool.rb +209 -0
  144. data/lib/ruby_llm/tool_call.rb +22 -0
  145. data/lib/ruby_llm/tool_executors.rb +125 -0
  146. data/lib/ruby_llm/transcription.rb +35 -0
  147. data/lib/ruby_llm/utils.rb +91 -0
  148. data/lib/ruby_llm/version.rb +5 -0
  149. data/lib/ruby_llm.rb +140 -0
  150. data/lib/tasks/models.rake +525 -0
  151. data/lib/tasks/release.rake +67 -0
  152. data/lib/tasks/ruby_llm.rake +15 -0
  153. data/lib/tasks/vcr.rake +92 -0
  154. 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Chat formatting for Perplexity provider
7
+ module Chat
8
+ module_function
9
+
10
+ def format_role(role)
11
+ role.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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