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.
- checksums.yaml +4 -4
- data/README.md +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +144 -47
- data/lib/ruby_llm/aliases.json +187 -17
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +31 -20
- data/lib/ruby_llm/configuration.rb +34 -1
- data/lib/ruby_llm/connection.rb +121 -0
- data/lib/ruby_llm/content.rb +27 -79
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +2 -1
- data/lib/ruby_llm/image.rb +15 -8
- data/lib/ruby_llm/message.rb +14 -6
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +26279 -2362
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
- data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +4 -3
- data/lib/ruby_llm/providers/gemini/media.rb +28 -111
- data/lib/ruby_llm/providers/gemini/models.rb +17 -23
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +48 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +48 -21
- data/lib/ruby_llm/providers/openai/models.rb +17 -18
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +48 -13
- data/lib/ruby_llm/utils.rb +27 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/models_update.rake +79 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +4 -2
- metadata +56 -32
- data/lib/ruby_llm/model_info.rb +0 -56
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
- data/lib/tasks/models.rake +0 -43
data/lib/ruby_llm/aliases.json
CHANGED
@@ -1,38 +1,208 @@
|
|
1
1
|
{
|
2
|
-
"
|
3
|
-
"
|
4
|
-
"
|
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
|
-
"
|
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
|
-
"
|
43
|
+
"openrouter": "anthropic/claude-3-sonnet",
|
44
|
+
"bedrock": "anthropic.claude-3-sonnet-20240229-v1:0:200k"
|
21
45
|
},
|
22
|
-
"claude-
|
23
|
-
"anthropic": "claude-
|
24
|
-
"
|
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-
|
27
|
-
"anthropic": "claude-
|
28
|
-
"
|
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
|
-
"
|
31
|
-
"
|
32
|
-
"
|
56
|
+
"deepseek-chat": {
|
57
|
+
"deepseek": "deepseek-chat",
|
58
|
+
"openrouter": "deepseek/deepseek-chat"
|
33
59
|
},
|
34
|
-
"
|
35
|
-
"
|
36
|
-
"
|
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
|
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
|
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
|
-
|
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:
|
34
|
+
def ask(message = nil, with: nil, &)
|
33
35
|
add_message role: :user, content: Content.new(message, with)
|
34
|
-
complete(&
|
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
|
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)
|
62
|
-
|
63
|
-
|
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(
|
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
|