ruby_llm_community 0.0.1 → 0.0.3

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +226 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +73 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +81 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +214 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +221 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +120 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +116 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +56 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +76 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +73 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +92 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +146 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +66 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +87 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +116 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +191 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +103 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. metadata +128 -7
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A class representing a file attachment.
5
+ class Attachment
6
+ attr_reader :source, :filename, :mime_type
7
+
8
+ def initialize(source, filename: nil)
9
+ @source = source
10
+ if url?
11
+ @source = URI source
12
+ @filename = filename || File.basename(@source.path).to_s
13
+ elsif path?
14
+ @source = Pathname.new source
15
+ @filename = filename || @source.basename.to_s
16
+ elsif active_storage?
17
+ @filename = filename || extract_filename_from_active_storage
18
+ else
19
+ @filename = filename
20
+ end
21
+
22
+ determine_mime_type
23
+ end
24
+
25
+ def url?
26
+ @source.is_a?(URI) || (@source.is_a?(String) && @source.match?(%r{^https?://}))
27
+ end
28
+
29
+ def path?
30
+ @source.is_a?(Pathname) || (@source.is_a?(String) && !url?)
31
+ end
32
+
33
+ def io_like?
34
+ @source.respond_to?(:read) && !path? && !active_storage?
35
+ end
36
+
37
+ def active_storage?
38
+ return false unless defined?(ActiveStorage)
39
+
40
+ @source.is_a?(ActiveStorage::Blob) ||
41
+ @source.is_a?(ActiveStorage::Attached::One) ||
42
+ @source.is_a?(ActiveStorage::Attached::Many)
43
+ end
44
+
45
+ def content
46
+ return @content if defined?(@content) && !@content.nil?
47
+
48
+ if url?
49
+ fetch_content
50
+ elsif path?
51
+ load_content_from_path
52
+ elsif active_storage?
53
+ load_content_from_active_storage
54
+ elsif io_like?
55
+ load_content_from_io
56
+ else
57
+ RubyLLM.logger.warn "Source is neither a URL, path, ActiveStorage, nor IO-like: #{@source.class}"
58
+ nil
59
+ end
60
+
61
+ @content
62
+ end
63
+
64
+ def encoded
65
+ Base64.strict_encode64(content)
66
+ end
67
+
68
+ def type
69
+ return :image if image?
70
+ return :audio if audio?
71
+ return :pdf if pdf?
72
+ return :text if text?
73
+
74
+ :unknown
75
+ end
76
+
77
+ def image?
78
+ RubyLLM::MimeType.image? mime_type
79
+ end
80
+
81
+ def audio?
82
+ RubyLLM::MimeType.audio? mime_type
83
+ end
84
+
85
+ def pdf?
86
+ RubyLLM::MimeType.pdf? mime_type
87
+ end
88
+
89
+ def text?
90
+ RubyLLM::MimeType.text? mime_type
91
+ end
92
+
93
+ def to_h
94
+ { type: type, source: @source }
95
+ end
96
+
97
+ private
98
+
99
+ def determine_mime_type
100
+ return @mime_type = active_storage_content_type if active_storage? && active_storage_content_type.present?
101
+
102
+ @mime_type = RubyLLM::MimeType.for(url? ? nil : @source, name: @filename)
103
+ @mime_type = RubyLLM::MimeType.for(content) if @mime_type == 'application/octet-stream'
104
+ @mime_type = 'audio/wav' if @mime_type == 'audio/x-wav' # Normalize WAV type
105
+ end
106
+
107
+ def fetch_content
108
+ response = Connection.basic.get @source.to_s
109
+ @content = response.body
110
+ end
111
+
112
+ def load_content_from_path
113
+ @content = File.read(@source)
114
+ end
115
+
116
+ def load_content_from_io
117
+ @source.rewind if @source.respond_to? :rewind
118
+ @content = @source.read
119
+ end
120
+
121
+ def load_content_from_active_storage
122
+ return unless defined?(ActiveStorage)
123
+
124
+ @content = case @source
125
+ when ActiveStorage::Blob
126
+ @source.download
127
+ when ActiveStorage::Attached::One
128
+ @source.blob&.download
129
+ when ActiveStorage::Attached::Many
130
+ # For multiple attachments, just take the first one
131
+ # This maintains the single-attachment interface
132
+ @source.blobs.first&.download
133
+ end
134
+ end
135
+
136
+ def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
137
+ return 'attachment' unless defined?(ActiveStorage)
138
+
139
+ case @source
140
+ when ActiveStorage::Blob
141
+ @source.filename.to_s
142
+ when ActiveStorage::Attached::One
143
+ @source.blob&.filename&.to_s || 'attachment'
144
+ when ActiveStorage::Attached::Many
145
+ @source.blobs.first&.filename&.to_s || 'attachment'
146
+ else
147
+ 'attachment'
148
+ end
149
+ end
150
+
151
+ def active_storage_content_type
152
+ return unless defined?(ActiveStorage)
153
+
154
+ case @source
155
+ when ActiveStorage::Blob
156
+ @source.content_type
157
+ when ActiveStorage::Attached::One
158
+ @source.blob&.content_type
159
+ when ActiveStorage::Attached::Many
160
+ @source.blobs.first&.content_type
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Represents a conversation with an AI model. Handles message history,
5
+ # streaming responses, and tool integration with a simple, conversational API.
6
+ #
7
+ # Example:
8
+ # chat = RubyLLM.chat
9
+ # chat.ask "What's the best way to learn Ruby?"
10
+ # chat.ask "Can you elaborate on that?"
11
+ class Chat
12
+ include Enumerable
13
+
14
+ attr_reader :model, :messages, :tools, :params, :headers, :schema
15
+
16
+ def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
17
+ if assume_model_exists && !provider
18
+ raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
19
+ end
20
+
21
+ @context = context
22
+ @config = context&.config || RubyLLM.config
23
+ model_id = model || @config.default_model
24
+ with_model(model_id, provider: provider, assume_exists: assume_model_exists)
25
+ @temperature = 0.7
26
+ @messages = []
27
+ @tools = {}
28
+ @cache_prompts = { system: false, user: false, tools: false }
29
+ @params = {}
30
+ @headers = {}
31
+ @schema = nil
32
+ @on = {
33
+ new_message: nil,
34
+ end_message: nil,
35
+ tool_call: nil,
36
+ tool_result: nil
37
+ }
38
+ end
39
+
40
+ def ask(message = nil, with: nil, &)
41
+ add_message role: :user, content: Content.new(message, with)
42
+ complete(&)
43
+ end
44
+
45
+ alias say ask
46
+
47
+ def with_instructions(instructions, replace: false)
48
+ @messages = @messages.reject { |msg| msg.role == :system } if replace
49
+
50
+ add_message role: :system, content: instructions
51
+ self
52
+ end
53
+
54
+ def with_tool(tool)
55
+ tool_instance = tool.is_a?(Class) ? tool.new : tool
56
+ @tools[tool_instance.name.to_sym] = tool_instance
57
+ self
58
+ end
59
+
60
+ def with_tools(*tools, replace: false)
61
+ @tools.clear if replace
62
+ tools.compact.each { |tool| with_tool tool }
63
+ self
64
+ end
65
+
66
+ def with_model(model_id, provider: nil, assume_exists: false)
67
+ @model, @provider = Models.resolve(model_id, provider:, assume_exists:, config: @config)
68
+ @connection = @provider.connection
69
+ self
70
+ end
71
+
72
+ def with_temperature(temperature)
73
+ @temperature = temperature
74
+ self
75
+ end
76
+
77
+ def with_context(context)
78
+ @context = context
79
+ @config = context.config
80
+ with_model(@model.id, provider: @provider.slug, assume_exists: true)
81
+ self
82
+ end
83
+
84
+ def with_params(**params)
85
+ @params = params
86
+ self
87
+ end
88
+
89
+ def with_headers(**headers)
90
+ @headers = headers
91
+ self
92
+ end
93
+
94
+ def with_schema(schema)
95
+ schema_instance = schema.is_a?(Class) ? schema.new : schema
96
+
97
+ # Accept both RubyLLM::Schema instances and plain JSON schemas
98
+ @schema = if schema_instance.respond_to?(:to_json_schema)
99
+ schema_instance.to_json_schema[:schema]
100
+ else
101
+ schema_instance
102
+ end
103
+
104
+ self
105
+ end
106
+
107
+ def on_new_message(&block)
108
+ @on[:new_message] = block
109
+ self
110
+ end
111
+
112
+ def on_end_message(&block)
113
+ @on[:end_message] = block
114
+ self
115
+ end
116
+
117
+ def on_tool_call(&block)
118
+ @on[:tool_call] = block
119
+ self
120
+ end
121
+
122
+ def on_tool_result(&block)
123
+ @on[:tool_result] = block
124
+ self
125
+ end
126
+
127
+ def each(&)
128
+ messages.each(&)
129
+ end
130
+
131
+ def cache_prompts(system: false, user: false, tools: false)
132
+ @cache_prompts = { system: system, user: user, tools: tools }
133
+ self
134
+ end
135
+
136
+ def complete(&) # rubocop:disable Metrics/PerceivedComplexity
137
+ response = @provider.complete(
138
+ messages,
139
+ tools: @tools,
140
+ temperature: @temperature,
141
+ model: @model.id,
142
+ cache_prompts: @cache_prompts.dup,
143
+ params: @params,
144
+ headers: @headers,
145
+ schema: @schema,
146
+ &wrap_streaming_block(&)
147
+ )
148
+
149
+ @on[:new_message]&.call unless block_given?
150
+
151
+ # Parse JSON if schema was set
152
+ if @schema && response.content.is_a?(String)
153
+ begin
154
+ response.content = JSON.parse(response.content)
155
+ rescue JSON::ParserError
156
+ # If parsing fails, keep content as string
157
+ end
158
+ end
159
+
160
+ add_message response
161
+ @on[:end_message]&.call(response)
162
+
163
+ if response.tool_call?
164
+ handle_tool_calls(response, &)
165
+ else
166
+ response
167
+ end
168
+ end
169
+
170
+ def add_message(message_or_attributes)
171
+ message = message_or_attributes.is_a?(Message) ? message_or_attributes : Message.new(message_or_attributes)
172
+ messages << message
173
+ message
174
+ end
175
+
176
+ def reset_messages!
177
+ @messages.clear
178
+ end
179
+
180
+ private
181
+
182
+ def wrap_streaming_block(&block)
183
+ return nil unless block_given?
184
+
185
+ first_chunk_received = false
186
+
187
+ proc do |chunk|
188
+ # Create message on first content chunk
189
+ unless first_chunk_received
190
+ first_chunk_received = true
191
+ @on[:new_message]&.call
192
+ end
193
+
194
+ # Pass chunk to user's block
195
+ block.call chunk
196
+ end
197
+ end
198
+
199
+ def handle_tool_calls(response, &)
200
+ halt_result = nil
201
+
202
+ response.tool_calls.each_value do |tool_call|
203
+ @on[:new_message]&.call
204
+ @on[:tool_call]&.call(tool_call)
205
+ result = execute_tool tool_call
206
+ @on[:tool_result]&.call(result)
207
+ message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
208
+ @on[:end_message]&.call(message)
209
+
210
+ halt_result = result if result.is_a?(Tool::Halt)
211
+ end
212
+
213
+ halt_result || complete(&)
214
+ end
215
+
216
+ def execute_tool(tool_call)
217
+ tool = tools[tool_call.name.to_sym]
218
+ args = tool_call.arguments
219
+ tool.call(args)
220
+ end
221
+
222
+ def instance_variables
223
+ super - %i[@connection @config]
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Chunk < Message
5
+ end
6
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Global configuration for RubyLLM. Manages API keys, default models,
5
+ # and provider-specific settings.
6
+ #
7
+ # Configure via:
8
+ # RubyLLM.configure do |config|
9
+ # config.openai_api_key = ENV['OPENAI_API_KEY']
10
+ # config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
11
+ # end
12
+ class Configuration
13
+ # Provider-specific configuration
14
+ attr_accessor :openai_api_key,
15
+ :openai_api_base,
16
+ :openai_organization_id,
17
+ :openai_project_id,
18
+ :openai_use_system_role,
19
+ :anthropic_api_key,
20
+ :gemini_api_key,
21
+ :deepseek_api_key,
22
+ :perplexity_api_key,
23
+ :bedrock_api_key,
24
+ :bedrock_secret_key,
25
+ :bedrock_region,
26
+ :bedrock_session_token,
27
+ :openrouter_api_key,
28
+ :ollama_api_base,
29
+ :gpustack_api_base,
30
+ :gpustack_api_key,
31
+ :mistral_api_key,
32
+ # Default models
33
+ :default_model,
34
+ :default_embedding_model,
35
+ :default_image_model,
36
+ # Connection configuration
37
+ :request_timeout,
38
+ :max_retries,
39
+ :retry_interval,
40
+ :retry_backoff_factor,
41
+ :retry_interval_randomness,
42
+ :http_proxy,
43
+ # Logging configuration
44
+ :logger,
45
+ :log_file,
46
+ :log_level,
47
+ :log_stream_debug
48
+
49
+ def initialize
50
+ # Connection configuration
51
+ @request_timeout = 120
52
+ @max_retries = 3
53
+ @retry_interval = 0.1
54
+ @retry_backoff_factor = 2
55
+ @retry_interval_randomness = 0.5
56
+ @http_proxy = nil
57
+
58
+ # Default models
59
+ @default_model = 'gpt-4.1-nano'
60
+ @default_embedding_model = 'text-embedding-3-small'
61
+ @default_image_model = 'gpt-image-1'
62
+
63
+ # Logging configuration
64
+ @log_file = $stdout
65
+ @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
66
+ @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
67
+ end
68
+
69
+ def instance_variables
70
+ super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Connection class for managing API connections to various providers.
5
+ class Connection
6
+ attr_reader :provider, :connection, :config
7
+
8
+ def self.basic(&)
9
+ Faraday.new do |f|
10
+ f.response :logger,
11
+ RubyLLM.logger,
12
+ bodies: false,
13
+ response: false,
14
+ errors: true,
15
+ headers: false,
16
+ log_level: :debug
17
+ f.response :raise_error
18
+ yield f if block_given?
19
+ end
20
+ end
21
+
22
+ def initialize(provider, config)
23
+ @provider = provider
24
+ @config = config
25
+
26
+ ensure_configured!
27
+ @connection ||= Faraday.new(provider.api_base) do |faraday|
28
+ setup_timeout(faraday)
29
+ setup_logging(faraday)
30
+ setup_retry(faraday)
31
+ setup_middleware(faraday)
32
+ setup_http_proxy(faraday)
33
+ end
34
+ end
35
+
36
+ def post(url, payload, &)
37
+ body = payload.is_a?(Hash) ? JSON.generate(payload, ascii_only: false) : payload
38
+ @connection.post url, body do |req|
39
+ req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
40
+ yield req if block_given?
41
+ end
42
+ end
43
+
44
+ def get(url, &)
45
+ @connection.get url do |req|
46
+ req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
47
+ yield req if block_given?
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def setup_timeout(faraday)
54
+ faraday.options.timeout = @config.request_timeout
55
+ end
56
+
57
+ def setup_logging(faraday)
58
+ faraday.response :logger,
59
+ RubyLLM.logger,
60
+ bodies: true,
61
+ response: true,
62
+ errors: true,
63
+ headers: false,
64
+ log_level: :debug do |logger|
65
+ logger.filter(%r{[A-Za-z0-9+/=]{100,}}, 'data":"[BASE64 DATA]"')
66
+ logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
67
+ end
68
+ end
69
+
70
+ def setup_retry(faraday)
71
+ faraday.request :retry, {
72
+ max: @config.max_retries,
73
+ interval: @config.retry_interval,
74
+ interval_randomness: @config.retry_interval_randomness,
75
+ backoff_factor: @config.retry_backoff_factor,
76
+ exceptions: retry_exceptions,
77
+ retry_statuses: [429, 500, 502, 503, 504, 529]
78
+ }
79
+ end
80
+
81
+ def setup_middleware(faraday)
82
+ faraday.request :json
83
+ faraday.response :json
84
+ faraday.adapter Faraday.default_adapter
85
+ faraday.use :llm_errors, provider: @provider
86
+ end
87
+
88
+ def setup_http_proxy(faraday)
89
+ return unless @config.http_proxy
90
+
91
+ faraday.proxy = @config.http_proxy
92
+ end
93
+
94
+ def retry_exceptions
95
+ [
96
+ Errno::ETIMEDOUT,
97
+ Timeout::Error,
98
+ Faraday::TimeoutError,
99
+ Faraday::ConnectionFailed,
100
+ Faraday::RetriableResponse,
101
+ RubyLLM::RateLimitError,
102
+ RubyLLM::ServerError,
103
+ RubyLLM::ServiceUnavailableError,
104
+ RubyLLM::OverloadedError
105
+ ]
106
+ end
107
+
108
+ def ensure_configured!
109
+ return if @provider.configured?
110
+
111
+ missing = @provider.configuration_requirements.reject { |req| @config.send(req) }
112
+ config_block = <<~RUBY
113
+ RubyLLM.configure do |config|
114
+ #{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
115
+ end
116
+ RUBY
117
+
118
+ raise ConfigurationError,
119
+ "#{@provider.name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
120
+ end
121
+
122
+ def instance_variables
123
+ super - %i[@config @connection]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Represents the content sent to or received from an LLM.
5
+ # Selects the appropriate attachment class based on the content type.
6
+ class Content
7
+ attr_reader :text, :attachments
8
+
9
+ def initialize(text = nil, attachments = nil)
10
+ @text = text
11
+ @attachments = []
12
+
13
+ process_attachments(attachments)
14
+ raise ArgumentError, 'Text and attachments cannot be both nil' if @text.nil? && @attachments.empty?
15
+ end
16
+
17
+ def add_attachment(source, filename: nil)
18
+ @attachments << Attachment.new(source, filename:)
19
+ self
20
+ end
21
+
22
+ def format
23
+ if @text && @attachments.empty?
24
+ @text
25
+ else
26
+ self
27
+ end
28
+ end
29
+
30
+ # For Rails serialization
31
+ def to_h
32
+ { text: @text, attachments: @attachments.map(&:to_h) }
33
+ end
34
+
35
+ private
36
+
37
+ def process_attachments_array_or_string(attachments)
38
+ Utils.to_safe_array(attachments).each do |file|
39
+ add_attachment(file)
40
+ end
41
+ end
42
+
43
+ def process_attachments(attachments)
44
+ if attachments.is_a?(Hash)
45
+ # Ignores types (like :image, :audio, :text, :pdf) since we have robust MIME type detection
46
+ attachments.each_value { |attachment| process_attachments_array_or_string(attachment) }
47
+ else
48
+ process_attachments_array_or_string attachments
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Holds per-call configs
5
+ class Context
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @connections = {}
11
+ end
12
+
13
+ def chat(*args, **kwargs, &)
14
+ Chat.new(*args, **kwargs, context: self, &)
15
+ end
16
+
17
+ def embed(*args, **kwargs, &)
18
+ Embedding.embed(*args, **kwargs, context: self, &)
19
+ end
20
+
21
+ def paint(*args, **kwargs, &)
22
+ Image.paint(*args, **kwargs, context: self, &)
23
+ end
24
+
25
+ def connection_for(provider_instance)
26
+ provider_instance.connection
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Core embedding interface. Provides a clean way to generate embeddings
5
+ # from text using various provider models.
6
+ class Embedding
7
+ attr_reader :vectors, :model, :input_tokens
8
+
9
+ def initialize(vectors:, model:, input_tokens: 0)
10
+ @vectors = vectors
11
+ @model = model
12
+ @input_tokens = input_tokens
13
+ end
14
+
15
+ def self.embed(text, # rubocop:disable Metrics/ParameterLists
16
+ model: nil,
17
+ provider: nil,
18
+ assume_model_exists: false,
19
+ context: nil,
20
+ dimensions: nil)
21
+ config = context&.config || RubyLLM.config
22
+ model ||= config.default_embedding_model
23
+ model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
24
+ config: config)
25
+ model_id = model.id
26
+
27
+ provider_instance.embed(text, model: model_id, dimensions:)
28
+ end
29
+ end
30
+ end