ruby_llm_community 0.0.1 → 0.0.2

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 +219 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +75 -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 +76 -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 +218 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +219 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -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 +65 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +61 -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 +63 -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 +90 -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 +139 -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 +58 -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 +86 -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 +115 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +190 -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 +97 -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,219 @@
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
+ @params = {}
29
+ @headers = {}
30
+ @schema = nil
31
+ @on = {
32
+ new_message: nil,
33
+ end_message: nil,
34
+ tool_call: nil,
35
+ tool_result: nil
36
+ }
37
+ end
38
+
39
+ def ask(message = nil, with: nil, &)
40
+ add_message role: :user, content: Content.new(message, with)
41
+ complete(&)
42
+ end
43
+
44
+ alias say ask
45
+
46
+ def with_instructions(instructions, replace: false)
47
+ @messages = @messages.reject { |msg| msg.role == :system } if replace
48
+
49
+ add_message role: :system, content: instructions
50
+ self
51
+ end
52
+
53
+ def with_tool(tool)
54
+ tool_instance = tool.is_a?(Class) ? tool.new : tool
55
+ @tools[tool_instance.name.to_sym] = tool_instance
56
+ self
57
+ end
58
+
59
+ def with_tools(*tools, replace: false)
60
+ @tools.clear if replace
61
+ tools.compact.each { |tool| with_tool tool }
62
+ self
63
+ end
64
+
65
+ def with_model(model_id, provider: nil, assume_exists: false)
66
+ @model, @provider = Models.resolve(model_id, provider:, assume_exists:, config: @config)
67
+ @connection = @provider.connection
68
+ self
69
+ end
70
+
71
+ def with_temperature(temperature)
72
+ @temperature = temperature
73
+ self
74
+ end
75
+
76
+ def with_context(context)
77
+ @context = context
78
+ @config = context.config
79
+ with_model(@model.id, provider: @provider.slug, assume_exists: true)
80
+ self
81
+ end
82
+
83
+ def with_params(**params)
84
+ @params = params
85
+ self
86
+ end
87
+
88
+ def with_headers(**headers)
89
+ @headers = headers
90
+ self
91
+ end
92
+
93
+ def with_schema(schema)
94
+ schema_instance = schema.is_a?(Class) ? schema.new : schema
95
+
96
+ # Accept both RubyLLM::Schema instances and plain JSON schemas
97
+ @schema = if schema_instance.respond_to?(:to_json_schema)
98
+ schema_instance.to_json_schema[:schema]
99
+ else
100
+ schema_instance
101
+ end
102
+
103
+ self
104
+ end
105
+
106
+ def on_new_message(&block)
107
+ @on[:new_message] = block
108
+ self
109
+ end
110
+
111
+ def on_end_message(&block)
112
+ @on[:end_message] = block
113
+ self
114
+ end
115
+
116
+ def on_tool_call(&block)
117
+ @on[:tool_call] = block
118
+ self
119
+ end
120
+
121
+ def on_tool_result(&block)
122
+ @on[:tool_result] = block
123
+ self
124
+ end
125
+
126
+ def each(&)
127
+ messages.each(&)
128
+ end
129
+
130
+ def complete(&) # rubocop:disable Metrics/PerceivedComplexity
131
+ response = @provider.complete(
132
+ messages,
133
+ tools: @tools,
134
+ temperature: @temperature,
135
+ model: @model.id,
136
+ params: @params,
137
+ headers: @headers,
138
+ schema: @schema,
139
+ &wrap_streaming_block(&)
140
+ )
141
+
142
+ @on[:new_message]&.call unless block_given?
143
+
144
+ # Parse JSON if schema was set
145
+ if @schema && response.content.is_a?(String)
146
+ begin
147
+ response.content = JSON.parse(response.content)
148
+ rescue JSON::ParserError
149
+ # If parsing fails, keep content as string
150
+ end
151
+ end
152
+
153
+ add_message response
154
+ @on[:end_message]&.call(response)
155
+
156
+ if response.tool_call?
157
+ handle_tool_calls(response, &)
158
+ else
159
+ response
160
+ end
161
+ end
162
+
163
+ def add_message(message_or_attributes)
164
+ message = message_or_attributes.is_a?(Message) ? message_or_attributes : Message.new(message_or_attributes)
165
+ messages << message
166
+ message
167
+ end
168
+
169
+ def reset_messages!
170
+ @messages.clear
171
+ end
172
+
173
+ private
174
+
175
+ def wrap_streaming_block(&block)
176
+ return nil unless block_given?
177
+
178
+ first_chunk_received = false
179
+
180
+ proc do |chunk|
181
+ # Create message on first content chunk
182
+ unless first_chunk_received
183
+ first_chunk_received = true
184
+ @on[:new_message]&.call
185
+ end
186
+
187
+ # Pass chunk to user's block
188
+ block.call chunk
189
+ end
190
+ end
191
+
192
+ def handle_tool_calls(response, &)
193
+ halt_result = nil
194
+
195
+ response.tool_calls.each_value do |tool_call|
196
+ @on[:new_message]&.call
197
+ @on[:tool_call]&.call(tool_call)
198
+ result = execute_tool tool_call
199
+ @on[:tool_result]&.call(result)
200
+ message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
201
+ @on[:end_message]&.call(message)
202
+
203
+ halt_result = result if result.is_a?(Tool::Halt)
204
+ end
205
+
206
+ halt_result || complete(&)
207
+ end
208
+
209
+ def execute_tool(tool_call)
210
+ tool = tools[tool_call.name.to_sym]
211
+ args = tool_call.arguments
212
+ tool.call(args)
213
+ end
214
+
215
+ def instance_variables
216
+ super - %i[@connection @config]
217
+ end
218
+ end
219
+ 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,75 @@
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_assume_model_exists,
48
+ :log_stream_debug
49
+
50
+ def initialize
51
+ # Connection configuration
52
+ @request_timeout = 120
53
+ @max_retries = 3
54
+ @retry_interval = 0.1
55
+ @retry_backoff_factor = 2
56
+ @retry_interval_randomness = 0.5
57
+ @http_proxy = nil
58
+
59
+ # Default models
60
+ @default_model = 'gpt-4.1-nano'
61
+ @default_embedding_model = 'text-embedding-3-small'
62
+ @default_image_model = 'gpt-image-1'
63
+
64
+ # Logging configuration
65
+ @log_file = $stdout
66
+ @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
67
+ @log_assume_model_exists = true
68
+ @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
69
+ end
70
+
71
+ def instance_variables
72
+ super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
73
+ end
74
+ end
75
+ 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