ruby_llm 1.2.0 → 1.3.0

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +144 -47
  4. data/lib/ruby_llm/aliases.json +187 -17
  5. data/lib/ruby_llm/attachment.rb +164 -0
  6. data/lib/ruby_llm/chat.rb +31 -20
  7. data/lib/ruby_llm/configuration.rb +34 -1
  8. data/lib/ruby_llm/connection.rb +121 -0
  9. data/lib/ruby_llm/content.rb +27 -79
  10. data/lib/ruby_llm/context.rb +30 -0
  11. data/lib/ruby_llm/embedding.rb +13 -5
  12. data/lib/ruby_llm/error.rb +2 -1
  13. data/lib/ruby_llm/image.rb +15 -8
  14. data/lib/ruby_llm/message.rb +14 -6
  15. data/lib/ruby_llm/mime_type.rb +67 -0
  16. data/lib/ruby_llm/model/info.rb +101 -0
  17. data/lib/ruby_llm/model/modalities.rb +22 -0
  18. data/lib/ruby_llm/model/pricing.rb +51 -0
  19. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  20. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  21. data/lib/ruby_llm/model.rb +7 -0
  22. data/lib/ruby_llm/models.json +26279 -2362
  23. data/lib/ruby_llm/models.rb +95 -14
  24. data/lib/ruby_llm/provider.rb +48 -90
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  26. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  27. data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
  28. data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
  29. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  30. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  31. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  33. data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
  34. data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  36. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  37. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  38. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  40. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  42. data/lib/ruby_llm/providers/gemini/images.rb +4 -3
  43. data/lib/ruby_llm/providers/gemini/media.rb +28 -111
  44. data/lib/ruby_llm/providers/gemini/models.rb +17 -23
  45. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini.rb +3 -3
  47. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  48. data/lib/ruby_llm/providers/ollama/media.rb +48 -0
  49. data/lib/ruby_llm/providers/ollama.rb +34 -0
  50. data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
  51. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  52. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  53. data/lib/ruby_llm/providers/openai/images.rb +3 -2
  54. data/lib/ruby_llm/providers/openai/media.rb +48 -21
  55. data/lib/ruby_llm/providers/openai/models.rb +17 -18
  56. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  57. data/lib/ruby_llm/providers/openai.rb +7 -5
  58. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  59. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  60. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  61. data/lib/ruby_llm/streaming.rb +48 -13
  62. data/lib/ruby_llm/utils.rb +27 -0
  63. data/lib/ruby_llm/version.rb +1 -1
  64. data/lib/ruby_llm.rb +15 -5
  65. data/lib/tasks/aliases.rake +235 -0
  66. data/lib/tasks/models_docs.rake +164 -121
  67. data/lib/tasks/models_update.rake +79 -0
  68. data/lib/tasks/release.rake +32 -0
  69. data/lib/tasks/vcr.rake +4 -2
  70. metadata +56 -32
  71. data/lib/ruby_llm/model_info.rb +0 -56
  72. data/lib/tasks/browser_helper.rb +0 -97
  73. data/lib/tasks/capability_generator.rb +0 -123
  74. data/lib/tasks/capability_scraper.rb +0 -224
  75. data/lib/tasks/cli_helper.rb +0 -22
  76. data/lib/tasks/code_validator.rb +0 -29
  77. data/lib/tasks/model_updater.rb +0 -66
  78. data/lib/tasks/models.rake +0 -43
@@ -1,38 +1,208 @@
1
1
  {
2
- "claude-3-5-sonnet": {
3
- "anthropic": "claude-3-5-sonnet-20241022",
4
- "bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
2
+ "chatgpt-4o": {
3
+ "openai": "chatgpt-4o-latest",
4
+ "openrouter": "openai/chatgpt-4o-latest"
5
+ },
6
+ "claude-2.0": {
7
+ "anthropic": "claude-2.0",
8
+ "openrouter": "anthropic/claude-2.0",
9
+ "bedrock": "anthropic.claude-v2:1:200k"
10
+ },
11
+ "claude-2.1": {
12
+ "anthropic": "claude-2.1",
13
+ "openrouter": "anthropic/claude-2.1",
14
+ "bedrock": "anthropic.claude-v2:1:200k"
5
15
  },
6
16
  "claude-3-5-haiku": {
7
17
  "anthropic": "claude-3-5-haiku-20241022",
18
+ "openrouter": "anthropic/claude-3.5-haiku",
8
19
  "bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0"
9
20
  },
21
+ "claude-3-5-sonnet": {
22
+ "anthropic": "claude-3-5-sonnet-20241022",
23
+ "openrouter": "anthropic/claude-3.5-sonnet",
24
+ "bedrock": "anthropic.claude-3-5-sonnet-20240620-v1:0:200k"
25
+ },
10
26
  "claude-3-7-sonnet": {
11
27
  "anthropic": "claude-3-7-sonnet-20250219",
28
+ "openrouter": "anthropic/claude-3.7-sonnet",
12
29
  "bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
13
30
  },
31
+ "claude-3-haiku": {
32
+ "anthropic": "claude-3-haiku-20240307",
33
+ "openrouter": "anthropic/claude-3-haiku",
34
+ "bedrock": "anthropic.claude-3-haiku-20240307-v1:0:200k"
35
+ },
14
36
  "claude-3-opus": {
15
37
  "anthropic": "claude-3-opus-20240229",
16
- "bedrock": "anthropic.claude-3-opus-20240229-v1:0"
38
+ "openrouter": "anthropic/claude-3-opus",
39
+ "bedrock": "anthropic.claude-3-opus-20240229-v1:0:200k"
17
40
  },
18
41
  "claude-3-sonnet": {
19
42
  "anthropic": "claude-3-sonnet-20240229",
20
- "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
43
+ "openrouter": "anthropic/claude-3-sonnet",
44
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0:200k"
21
45
  },
22
- "claude-3-haiku": {
23
- "anthropic": "claude-3-haiku-20240307",
24
- "bedrock": "anthropic.claude-3-haiku-20240307-v1:0"
46
+ "claude-opus-4": {
47
+ "anthropic": "claude-opus-4-20250514",
48
+ "openrouter": "anthropic/claude-opus-4",
49
+ "bedrock": "us.anthropic.claude-opus-4-20250514-v1:0"
25
50
  },
26
- "claude-3": {
27
- "anthropic": "claude-3-sonnet-20240229",
28
- "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
51
+ "claude-sonnet-4": {
52
+ "anthropic": "claude-sonnet-4-20250514",
53
+ "openrouter": "anthropic/claude-sonnet-4",
54
+ "bedrock": "us.anthropic.claude-sonnet-4-20250514-v1:0"
29
55
  },
30
- "claude-2": {
31
- "anthropic": "claude-2.0",
32
- "bedrock": "anthropic.claude-2.0"
56
+ "deepseek-chat": {
57
+ "deepseek": "deepseek-chat",
58
+ "openrouter": "deepseek/deepseek-chat"
33
59
  },
34
- "claude-2-1": {
35
- "anthropic": "claude-2.1",
36
- "bedrock": "anthropic.claude-2.1"
60
+ "gemini-2.0-flash-001": {
61
+ "gemini": "gemini-2.0-flash-001",
62
+ "openrouter": "google/gemini-2.0-flash-001"
63
+ },
64
+ "gemini-2.0-flash-lite-001": {
65
+ "gemini": "gemini-2.0-flash-lite-001",
66
+ "openrouter": "google/gemini-2.0-flash-lite-001"
67
+ },
68
+ "gemini-2.5-flash-preview-05-20": {
69
+ "gemini": "gemini-2.5-flash-preview-05-20",
70
+ "openrouter": "google/gemini-2.5-flash-preview-05-20"
71
+ },
72
+ "gemini-2.5-pro-exp-03-25": {
73
+ "gemini": "gemini-2.5-pro-exp-03-25",
74
+ "openrouter": "google/gemini-2.5-pro-exp-03-25"
75
+ },
76
+ "gemma-3-12b-it": {
77
+ "gemini": "gemma-3-12b-it",
78
+ "openrouter": "google/gemma-3-12b-it"
79
+ },
80
+ "gemma-3-27b-it": {
81
+ "gemini": "gemma-3-27b-it",
82
+ "openrouter": "google/gemma-3-27b-it"
83
+ },
84
+ "gemma-3-4b-it": {
85
+ "gemini": "gemma-3-4b-it",
86
+ "openrouter": "google/gemma-3-4b-it"
87
+ },
88
+ "gpt-3.5-turbo": {
89
+ "openai": "gpt-3.5-turbo",
90
+ "openrouter": "openai/gpt-3.5-turbo"
91
+ },
92
+ "gpt-3.5-turbo-0125": {
93
+ "openai": "gpt-3.5-turbo-0125",
94
+ "openrouter": "openai/gpt-3.5-turbo-0125"
95
+ },
96
+ "gpt-3.5-turbo-1106": {
97
+ "openai": "gpt-3.5-turbo-1106",
98
+ "openrouter": "openai/gpt-3.5-turbo-1106"
99
+ },
100
+ "gpt-3.5-turbo-16k": {
101
+ "openai": "gpt-3.5-turbo-16k",
102
+ "openrouter": "openai/gpt-3.5-turbo-16k"
103
+ },
104
+ "gpt-3.5-turbo-instruct": {
105
+ "openai": "gpt-3.5-turbo-instruct",
106
+ "openrouter": "openai/gpt-3.5-turbo-instruct"
107
+ },
108
+ "gpt-4": {
109
+ "openai": "gpt-4",
110
+ "openrouter": "openai/gpt-4"
111
+ },
112
+ "gpt-4-1106-preview": {
113
+ "openai": "gpt-4-1106-preview",
114
+ "openrouter": "openai/gpt-4-1106-preview"
115
+ },
116
+ "gpt-4-turbo": {
117
+ "openai": "gpt-4-turbo",
118
+ "openrouter": "openai/gpt-4-turbo"
119
+ },
120
+ "gpt-4-turbo-preview": {
121
+ "openai": "gpt-4-turbo-preview",
122
+ "openrouter": "openai/gpt-4-turbo-preview"
123
+ },
124
+ "gpt-4.1": {
125
+ "openai": "gpt-4.1",
126
+ "openrouter": "openai/gpt-4.1"
127
+ },
128
+ "gpt-4.1-mini": {
129
+ "openai": "gpt-4.1-mini",
130
+ "openrouter": "openai/gpt-4.1-mini"
131
+ },
132
+ "gpt-4.1-nano": {
133
+ "openai": "gpt-4.1-nano",
134
+ "openrouter": "openai/gpt-4.1-nano"
135
+ },
136
+ "gpt-4.5-preview": {
137
+ "openai": "gpt-4.5-preview",
138
+ "openrouter": "openai/gpt-4.5-preview"
139
+ },
140
+ "gpt-4o": {
141
+ "openai": "gpt-4o",
142
+ "openrouter": "openai/gpt-4o"
143
+ },
144
+ "gpt-4o-2024-05-13": {
145
+ "openai": "gpt-4o-2024-05-13",
146
+ "openrouter": "openai/gpt-4o-2024-05-13"
147
+ },
148
+ "gpt-4o-2024-08-06": {
149
+ "openai": "gpt-4o-2024-08-06",
150
+ "openrouter": "openai/gpt-4o-2024-08-06"
151
+ },
152
+ "gpt-4o-2024-11-20": {
153
+ "openai": "gpt-4o-2024-11-20",
154
+ "openrouter": "openai/gpt-4o-2024-11-20"
155
+ },
156
+ "gpt-4o-mini": {
157
+ "openai": "gpt-4o-mini",
158
+ "openrouter": "openai/gpt-4o-mini"
159
+ },
160
+ "gpt-4o-mini-2024-07-18": {
161
+ "openai": "gpt-4o-mini-2024-07-18",
162
+ "openrouter": "openai/gpt-4o-mini-2024-07-18"
163
+ },
164
+ "gpt-4o-mini-search-preview": {
165
+ "openai": "gpt-4o-mini-search-preview",
166
+ "openrouter": "openai/gpt-4o-mini-search-preview"
167
+ },
168
+ "gpt-4o-search-preview": {
169
+ "openai": "gpt-4o-search-preview",
170
+ "openrouter": "openai/gpt-4o-search-preview"
171
+ },
172
+ "o1": {
173
+ "openai": "o1",
174
+ "openrouter": "openai/o1"
175
+ },
176
+ "o1-mini": {
177
+ "openai": "o1-mini",
178
+ "openrouter": "openai/o1-mini"
179
+ },
180
+ "o1-mini-2024-09-12": {
181
+ "openai": "o1-mini-2024-09-12",
182
+ "openrouter": "openai/o1-mini-2024-09-12"
183
+ },
184
+ "o1-preview": {
185
+ "openai": "o1-preview",
186
+ "openrouter": "openai/o1-preview"
187
+ },
188
+ "o1-preview-2024-09-12": {
189
+ "openai": "o1-preview-2024-09-12",
190
+ "openrouter": "openai/o1-preview-2024-09-12"
191
+ },
192
+ "o1-pro": {
193
+ "openai": "o1-pro",
194
+ "openrouter": "openai/o1-pro"
195
+ },
196
+ "o3": {
197
+ "openai": "o3",
198
+ "openrouter": "openai/o3"
199
+ },
200
+ "o3-mini": {
201
+ "openai": "o3-mini",
202
+ "openrouter": "openai/o3-mini"
203
+ },
204
+ "o4-mini": {
205
+ "openai": "o4-mini",
206
+ "openrouter": "openai/o4-mini"
37
207
  }
38
208
  }
@@ -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(@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
data/lib/ruby_llm/chat.rb CHANGED
@@ -8,17 +8,19 @@ module RubyLLM
8
8
  # chat = RubyLLM.chat
9
9
  # chat.ask "What's the best way to learn Ruby?"
10
10
  # chat.ask "Can you elaborate on that?"
11
- class Chat # rubocop:disable Metrics/ClassLength
11
+ class Chat
12
12
  include Enumerable
13
13
 
14
14
  attr_reader :model, :messages, :tools
15
15
 
16
- def initialize(model: nil, provider: nil, assume_model_exists: false) # rubocop:disable Metrics/MethodLength
16
+ def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
17
17
  if assume_model_exists && !provider
18
18
  raise ArgumentError, 'Provider must be specified if assume_model_exists is true'
19
19
  end
20
20
 
21
- model_id = model || RubyLLM.config.default_model
21
+ @context = context
22
+ @config = context&.config || RubyLLM.config
23
+ model_id = model || @config.default_model
22
24
  with_model(model_id, provider: provider, assume_exists: assume_model_exists)
23
25
  @temperature = 0.7
24
26
  @messages = []
@@ -29,22 +31,22 @@ module RubyLLM
29
31
  }
30
32
  end
31
33
 
32
- def ask(message = nil, with: {}, &block)
34
+ def ask(message = nil, with: nil, &)
33
35
  add_message role: :user, content: Content.new(message, with)
34
- complete(&block)
36
+ complete(&)
35
37
  end
36
38
 
37
39
  alias say ask
38
40
 
39
41
  def with_instructions(instructions, replace: false)
40
- @messages = @messages.reject! { |msg| msg.role == :system } if replace
42
+ @messages = @messages.reject { |msg| msg.role == :system } if replace
41
43
 
42
44
  add_message role: :system, content: instructions
43
45
  self
44
46
  end
45
47
 
46
48
  def with_tool(tool)
47
- unless @model.supports_functions
49
+ unless @model.supports_functions?
48
50
  raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
49
51
  end
50
52
 
@@ -58,18 +60,9 @@ module RubyLLM
58
60
  self
59
61
  end
60
62
 
61
- def with_model(model_id, provider: nil, assume_exists: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
62
- if assume_exists
63
- raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
64
-
65
- @provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
66
- @model = Struct.new(:id, :provider, :supports_functions, :supports_vision).new(model_id, provider, true, true)
67
- RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
68
- 'Capabilities may not be accurately reflected.'
69
- else
70
- @model = Models.find model_id, provider
71
- @provider = Provider.providers[@model.provider.to_sym] || raise(Error, "Unknown provider: #{@model.provider}")
72
- end
63
+ def with_model(model_id, provider: nil, assume_exists: false)
64
+ @model, @provider = Models.resolve(model_id, provider:, assume_exists:)
65
+ @connection = @context ? @context.connection_for(@provider) : @provider.connection(@config)
73
66
  self
74
67
  end
75
68
 
@@ -78,6 +71,13 @@ module RubyLLM
78
71
  self
79
72
  end
80
73
 
74
+ def with_context(context)
75
+ @context = context
76
+ @config = context.config
77
+ with_model(@model.id, provider: @provider.slug, assume_exists: true)
78
+ self
79
+ end
80
+
81
81
  def on_new_message(&block)
82
82
  @on[:new_message] = block
83
83
  self
@@ -94,7 +94,14 @@ module RubyLLM
94
94
 
95
95
  def complete(&)
96
96
  @on[:new_message]&.call
97
- response = @provider.complete(messages, tools: @tools, temperature: @temperature, model: @model.id, &)
97
+ response = @provider.complete(
98
+ messages,
99
+ tools: @tools,
100
+ temperature: @temperature,
101
+ model: @model.id,
102
+ connection: @connection,
103
+ &
104
+ )
98
105
  @on[:end_message]&.call(response)
99
106
 
100
107
  add_message response
@@ -111,6 +118,10 @@ module RubyLLM
111
118
  message
112
119
  end
113
120
 
121
+ def reset_messages!
122
+ @messages.clear
123
+ end
124
+
114
125
  private
115
126
 
116
127
  def handle_tool_calls(response, &)
@@ -13,6 +13,8 @@ module RubyLLM
13
13
  # Provider-specific configuration
14
14
  attr_accessor :openai_api_key,
15
15
  :openai_api_base,
16
+ :openai_organization_id,
17
+ :openai_project_id,
16
18
  :anthropic_api_key,
17
19
  :gemini_api_key,
18
20
  :deepseek_api_key,
@@ -20,6 +22,8 @@ module RubyLLM
20
22
  :bedrock_secret_key,
21
23
  :bedrock_region,
22
24
  :bedrock_session_token,
25
+ :openrouter_api_key,
26
+ :ollama_api_base,
23
27
  # Default models
24
28
  :default_model,
25
29
  :default_embedding_model,
@@ -29,7 +33,12 @@ module RubyLLM
29
33
  :max_retries,
30
34
  :retry_interval,
31
35
  :retry_backoff_factor,
32
- :retry_interval_randomness
36
+ :retry_interval_randomness,
37
+ :http_proxy,
38
+ # Logging configuration
39
+ :log_file,
40
+ :log_level,
41
+ :log_assume_model_exists
33
42
 
34
43
  def initialize
35
44
  # Connection configuration
@@ -38,11 +47,35 @@ module RubyLLM
38
47
  @retry_interval = 0.1
39
48
  @retry_backoff_factor = 2
40
49
  @retry_interval_randomness = 0.5
50
+ @http_proxy = nil
41
51
 
42
52
  # Default models
43
53
  @default_model = 'gpt-4.1-nano'
44
54
  @default_embedding_model = 'text-embedding-3-small'
45
55
  @default_image_model = 'dall-e-3'
56
+
57
+ # Logging configuration
58
+ @log_file = $stdout
59
+ @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
60
+ @log_assume_model_exists = true
61
+ end
62
+
63
+ def inspect
64
+ redacted = lambda do |name, value|
65
+ if name.match?(/_id|_key|_secret|_token$/)
66
+ value.nil? ? 'nil' : '[FILTERED]'
67
+ else
68
+ value
69
+ end
70
+ end
71
+
72
+ inspection = instance_variables.map do |ivar|
73
+ name = ivar.to_s.delete_prefix('@')
74
+ value = redacted[name, instance_variable_get(ivar)]
75
+ "#{name}: #{value}"
76
+ end.join(', ')
77
+
78
+ "#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
46
79
  end
47
80
  end
48
81
  end
@@ -0,0 +1,121 @@
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(@config)) 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(@config) 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(@config) 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?(@config)
110
+
111
+ config_block = <<~RUBY
112
+ RubyLLM.configure do |config|
113
+ #{@provider.missing_configs(@config).map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
114
+ end
115
+ RUBY
116
+
117
+ raise ConfigurationError,
118
+ "#{@provider.slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
119
+ end
120
+ end
121
+ end