ruby_llm 1.5.1 → 1.6.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/ruby_llm/active_record/acts_as.rb +46 -6
  4. data/lib/ruby_llm/aliases.json +27 -3
  5. data/lib/ruby_llm/chat.rb +27 -6
  6. data/lib/ruby_llm/configuration.rb +7 -18
  7. data/lib/ruby_llm/connection.rb +11 -6
  8. data/lib/ruby_llm/context.rb +2 -3
  9. data/lib/ruby_llm/embedding.rb +3 -4
  10. data/lib/ruby_llm/error.rb +2 -2
  11. data/lib/ruby_llm/image.rb +3 -4
  12. data/lib/ruby_llm/message.rb +4 -0
  13. data/lib/ruby_llm/models.json +7306 -6676
  14. data/lib/ruby_llm/models.rb +22 -31
  15. data/lib/ruby_llm/provider.rb +150 -89
  16. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -2
  17. data/lib/ruby_llm/providers/anthropic/chat.rb +1 -1
  18. data/lib/ruby_llm/providers/anthropic/embeddings.rb +1 -1
  19. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  20. data/lib/ruby_llm/providers/anthropic/models.rb +1 -1
  21. data/lib/ruby_llm/providers/anthropic/streaming.rb +1 -1
  22. data/lib/ruby_llm/providers/anthropic/tools.rb +1 -1
  23. data/lib/ruby_llm/providers/anthropic.rb +17 -22
  24. data/lib/ruby_llm/providers/bedrock/capabilities.rb +3 -63
  25. data/lib/ruby_llm/providers/bedrock/chat.rb +5 -4
  26. data/lib/ruby_llm/providers/bedrock/media.rb +1 -1
  27. data/lib/ruby_llm/providers/bedrock/models.rb +5 -6
  28. data/lib/ruby_llm/providers/bedrock/signing.rb +1 -1
  29. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +5 -4
  30. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +1 -1
  31. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +1 -1
  32. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +1 -1
  33. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +1 -1
  34. data/lib/ruby_llm/providers/bedrock/streaming.rb +1 -1
  35. data/lib/ruby_llm/providers/bedrock.rb +26 -31
  36. data/lib/ruby_llm/providers/deepseek/capabilities.rb +16 -57
  37. data/lib/ruby_llm/providers/deepseek/chat.rb +1 -1
  38. data/lib/ruby_llm/providers/deepseek.rb +12 -17
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +1 -1
  40. data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +1 -1
  42. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  43. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  44. data/lib/ruby_llm/providers/gemini/models.rb +1 -1
  45. data/lib/ruby_llm/providers/gemini/streaming.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini/tools.rb +1 -7
  47. data/lib/ruby_llm/providers/gemini.rb +18 -23
  48. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  49. data/lib/ruby_llm/providers/gpustack/models.rb +1 -1
  50. data/lib/ruby_llm/providers/gpustack.rb +16 -19
  51. data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
  52. data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
  53. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -1
  54. data/lib/ruby_llm/providers/mistral/models.rb +1 -1
  55. data/lib/ruby_llm/providers/mistral.rb +14 -19
  56. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  57. data/lib/ruby_llm/providers/ollama/media.rb +1 -1
  58. data/lib/ruby_llm/providers/ollama.rb +13 -18
  59. data/lib/ruby_llm/providers/openai/capabilities.rb +2 -2
  60. data/lib/ruby_llm/providers/openai/chat.rb +2 -2
  61. data/lib/ruby_llm/providers/openai/embeddings.rb +1 -1
  62. data/lib/ruby_llm/providers/openai/images.rb +1 -1
  63. data/lib/ruby_llm/providers/openai/media.rb +1 -1
  64. data/lib/ruby_llm/providers/openai/models.rb +1 -1
  65. data/lib/ruby_llm/providers/openai/streaming.rb +1 -1
  66. data/lib/ruby_llm/providers/openai/tools.rb +1 -1
  67. data/lib/ruby_llm/providers/openai.rb +24 -36
  68. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  69. data/lib/ruby_llm/providers/openrouter.rb +9 -14
  70. data/lib/ruby_llm/providers/perplexity/capabilities.rb +1 -30
  71. data/lib/ruby_llm/providers/perplexity/chat.rb +1 -1
  72. data/lib/ruby_llm/providers/perplexity/models.rb +1 -1
  73. data/lib/ruby_llm/providers/perplexity.rb +13 -18
  74. data/lib/ruby_llm/stream_accumulator.rb +3 -3
  75. data/lib/ruby_llm/streaming.rb +16 -3
  76. data/lib/ruby_llm/tool.rb +19 -0
  77. data/lib/ruby_llm/version.rb +1 -1
  78. data/lib/tasks/models_docs.rake +18 -11
  79. data/lib/tasks/models_update.rake +5 -4
  80. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 854c31993769b5123faf650081276dae4026907d467514c905739b8737220b55
4
- data.tar.gz: 408fb8253e5fdbf82bc3f3c3cc737e313d9835bd8023c1431cc4e6adc2a09be9
3
+ metadata.gz: b775b3cd7552709ea083109f554e53356fba6e33a81c4e725fbbb90d218bec7b
4
+ data.tar.gz: 16aee5696b33b7dc16383e0a5612e3d4c0f7d65d8fadfe20ebc95f13eea09d3f
5
5
  SHA512:
6
- metadata.gz: c5572a959bcd8f314d1af4d66bb363cbe9eaa783fa8cbd6e051d3b0e471da49e87a895724def790fad59589d08eb5eb259ffd8dbe1f09d70274a900ae9575af4
7
- data.tar.gz: 191f6f8aadc239998b0346e6a3e54fa4ace1d39fd56947693d7bd26e4248bfccc7894410f2f758bc456750cbef602b1577e7f3ca8a7c3a986077a7e957b7aa13
6
+ metadata.gz: ab67b23197bd8102b0e999e9cca92f5354dd1b3b1224da66c153f8959b2c9b4113ae0fd0ea3754729148b1a27307809daf59a2a511496d6ce6da900226daec62
7
+ data.tar.gz: 3302b8ef76c8dc0cad8f505b89d5cbd39bf68c02a051e1828f832c9854e3db944703dadf5d5a1ca30f9cd439b916bbbf2e6c5bc578480d81de4a66ef7bf3ff56
data/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
  </div>
33
33
 
34
34
  <div class="badge-container">
35
- <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg?a=1" alt="Gem Version" /></a>
35
+ <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg?a=2" alt="Gem Version" /></a>
36
36
  <a href="https://github.com/testdouble/standard"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Ruby Style Guide" /></a>
37
37
  <a href="https://rubygems.org/gems/ruby_llm"><img alt="Gem Downloads" src="https://img.shields.io/gem/dt/ruby_llm"></a>
38
38
  <a href="https://codecov.io/gh/crmne/ruby_llm"><img src="https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg" alt="codecov" /></a>
@@ -96,8 +96,7 @@ module RubyLLM
96
96
  @chat.add_message(msg.to_llm)
97
97
  end
98
98
 
99
- @chat.on_new_message { persist_new_message }
100
- .on_end_message { |msg| persist_message_completion(msg) }
99
+ setup_persistence_callbacks
101
100
  end
102
101
 
103
102
  def with_instructions(instructions, replace: false)
@@ -139,18 +138,47 @@ module RubyLLM
139
138
  self
140
139
  end
141
140
 
141
+ def with_headers(...)
142
+ to_llm.with_headers(...)
143
+ self
144
+ end
145
+
142
146
  def with_schema(...)
143
147
  to_llm.with_schema(...)
144
148
  self
145
149
  end
146
150
 
147
- def on_new_message(...)
148
- to_llm.on_new_message(...)
151
+ def on_new_message(&block)
152
+ to_llm
153
+
154
+ existing_callback = @chat.instance_variable_get(:@on)[:new_message]
155
+
156
+ @chat.on_new_message do
157
+ existing_callback&.call
158
+ block&.call
159
+ end
149
160
  self
150
161
  end
151
162
 
152
- def on_end_message(...)
153
- to_llm.on_end_message(...)
163
+ def on_end_message(&block)
164
+ to_llm
165
+
166
+ existing_callback = @chat.instance_variable_get(:@on)[:end_message]
167
+
168
+ @chat.on_end_message do |msg|
169
+ existing_callback&.call(msg)
170
+ block&.call(msg)
171
+ end
172
+ self
173
+ end
174
+
175
+ def on_tool_call(...)
176
+ to_llm.on_tool_call(...)
177
+ self
178
+ end
179
+
180
+ def on_tool_result(...)
181
+ to_llm.on_tool_result(...)
154
182
  self
155
183
  end
156
184
 
@@ -179,6 +207,18 @@ module RubyLLM
179
207
 
180
208
  private
181
209
 
210
+ def setup_persistence_callbacks
211
+ # Only set up once per chat instance
212
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
213
+
214
+ # Set up persistence callbacks (user callbacks will be chained via on_new_message/on_end_message methods)
215
+ @chat.on_new_message { persist_new_message }
216
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
217
+
218
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
219
+ @chat
220
+ end
221
+
182
222
  def persist_new_message
183
223
  @message = messages.create!(role: :assistant, content: String.new)
184
224
  end
@@ -29,13 +29,17 @@
29
29
  "bedrock": "anthropic.claude-3-opus-20240229-v1:0:200k"
30
30
  },
31
31
  "claude-3-sonnet": {
32
- "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0",
33
- "openrouter": "anthropic/claude-3-sonnet"
32
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
34
33
  },
35
34
  "claude-opus-4": {
36
35
  "anthropic": "claude-opus-4-20250514",
37
36
  "openrouter": "anthropic/claude-opus-4",
38
- "bedrock": "us.anthropic.claude-opus-4-20250514-v1:0"
37
+ "bedrock": "us.anthropic.claude-opus-4-1-20250805-v1:0"
38
+ },
39
+ "claude-opus-4-1": {
40
+ "anthropic": "claude-opus-4-1-20250805",
41
+ "openrouter": "anthropic/claude-opus-4.1",
42
+ "bedrock": "us.anthropic.claude-opus-4-1-20250805-v1:0"
39
43
  },
40
44
  "claude-sonnet-4": {
41
45
  "anthropic": "claude-sonnet-4-20250514",
@@ -162,6 +166,26 @@
162
166
  "openai": "gpt-4o-search-preview",
163
167
  "openrouter": "openai/gpt-4o-search-preview"
164
168
  },
169
+ "gpt-5": {
170
+ "openai": "gpt-5",
171
+ "openrouter": "openai/gpt-5"
172
+ },
173
+ "gpt-5-mini": {
174
+ "openai": "gpt-5-mini",
175
+ "openrouter": "openai/gpt-5-mini"
176
+ },
177
+ "gpt-5-nano": {
178
+ "openai": "gpt-5-nano",
179
+ "openrouter": "openai/gpt-5-nano"
180
+ },
181
+ "gpt-oss-120b": {
182
+ "openai": "gpt-oss-120b",
183
+ "openrouter": "openai/gpt-oss-120b"
184
+ },
185
+ "gpt-oss-20b": {
186
+ "openai": "gpt-oss-20b",
187
+ "openrouter": "openai/gpt-oss-20b"
188
+ },
165
189
  "o1": {
166
190
  "openai": "o1",
167
191
  "openrouter": "openai/o1"
data/lib/ruby_llm/chat.rb CHANGED
@@ -11,7 +11,7 @@ module RubyLLM
11
11
  class Chat
12
12
  include Enumerable
13
13
 
14
- attr_reader :model, :messages, :tools, :params, :schema
14
+ attr_reader :model, :messages, :tools, :params, :headers, :schema
15
15
 
16
16
  def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
17
17
  if assume_model_exists && !provider
@@ -26,11 +26,13 @@ module RubyLLM
26
26
  @messages = []
27
27
  @tools = {}
28
28
  @params = {}
29
+ @headers = {}
29
30
  @schema = nil
30
31
  @on = {
31
32
  new_message: nil,
32
33
  end_message: nil,
33
- tool_call: nil
34
+ tool_call: nil,
35
+ tool_result: nil
34
36
  }
35
37
  end
36
38
 
@@ -64,8 +66,8 @@ module RubyLLM
64
66
  end
65
67
 
66
68
  def with_model(model_id, provider: nil, assume_exists: false)
67
- @model, @provider = Models.resolve(model_id, provider:, assume_exists:)
68
- @connection = @context ? @context.connection_for(@provider) : @provider.connection(@config)
69
+ @model, @provider = Models.resolve(model_id, provider:, assume_exists:, config: @config)
70
+ @connection = @provider.connection
69
71
  self
70
72
  end
71
73
 
@@ -86,6 +88,11 @@ module RubyLLM
86
88
  self
87
89
  end
88
90
 
91
+ def with_headers(**headers)
92
+ @headers = headers
93
+ self
94
+ end
95
+
89
96
  def with_schema(schema, force: false)
90
97
  unless force || @model.structured_output?
91
98
  raise UnsupportedStructuredOutputError, "Model #{@model.id} doesn't support structured output"
@@ -118,6 +125,11 @@ module RubyLLM
118
125
  self
119
126
  end
120
127
 
128
+ def on_tool_result(&block)
129
+ @on[:tool_result] = block
130
+ self
131
+ end
132
+
121
133
  def each(&)
122
134
  messages.each(&)
123
135
  end
@@ -128,8 +140,8 @@ module RubyLLM
128
140
  tools: @tools,
129
141
  temperature: @temperature,
130
142
  model: @model.id,
131
- connection: @connection,
132
143
  params: @params,
144
+ headers: @headers,
133
145
  schema: @schema,
134
146
  &wrap_streaming_block(&)
135
147
  )
@@ -185,15 +197,20 @@ module RubyLLM
185
197
  end
186
198
 
187
199
  def handle_tool_calls(response, &)
200
+ halt_result = nil
201
+
188
202
  response.tool_calls.each_value do |tool_call|
189
203
  @on[:new_message]&.call
190
204
  @on[:tool_call]&.call(tool_call)
191
205
  result = execute_tool tool_call
206
+ @on[:tool_result]&.call(result)
192
207
  message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
193
208
  @on[:end_message]&.call(message)
209
+
210
+ halt_result = result if result.is_a?(Tool::Halt)
194
211
  end
195
212
 
196
- complete(&)
213
+ halt_result || complete(&)
197
214
  end
198
215
 
199
216
  def execute_tool(tool_call)
@@ -201,5 +218,9 @@ module RubyLLM
201
218
  args = tool_call.arguments
202
219
  tool.call(args)
203
220
  end
221
+
222
+ def instance_variables
223
+ super - %i[@connection @config]
224
+ end
204
225
  end
205
226
  end
@@ -15,6 +15,7 @@ module RubyLLM
15
15
  :openai_api_base,
16
16
  :openai_organization_id,
17
17
  :openai_project_id,
18
+ :openai_use_system_role,
18
19
  :anthropic_api_key,
19
20
  :gemini_api_key,
20
21
  :deepseek_api_key,
@@ -43,7 +44,8 @@ module RubyLLM
43
44
  :logger,
44
45
  :log_file,
45
46
  :log_level,
46
- :log_assume_model_exists
47
+ :log_assume_model_exists,
48
+ :log_stream_debug
47
49
 
48
50
  def initialize
49
51
  # Connection configuration
@@ -57,30 +59,17 @@ module RubyLLM
57
59
  # Default models
58
60
  @default_model = 'gpt-4.1-nano'
59
61
  @default_embedding_model = 'text-embedding-3-small'
60
- @default_image_model = 'dall-e-3'
62
+ @default_image_model = 'gpt-image-1'
61
63
 
62
64
  # Logging configuration
63
65
  @log_file = $stdout
64
66
  @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
65
67
  @log_assume_model_exists = true
68
+ @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
66
69
  end
67
70
 
68
- def inspect
69
- redacted = lambda do |name, value|
70
- if name.match?(/_id|_key|_secret|_token$/)
71
- value.nil? ? 'nil' : '[FILTERED]'
72
- else
73
- value
74
- end
75
- end
76
-
77
- inspection = instance_variables.map do |ivar|
78
- name = ivar.to_s.delete_prefix('@')
79
- value = redacted[name, instance_variable_get(ivar)]
80
- "#{name}: #{value}"
81
- end.join(', ')
82
-
83
- "#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
71
+ def instance_variables
72
+ super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
84
73
  end
85
74
  end
86
75
  end
@@ -24,7 +24,7 @@ module RubyLLM
24
24
  @config = config
25
25
 
26
26
  ensure_configured!
27
- @connection ||= Faraday.new(provider.api_base(@config)) do |faraday|
27
+ @connection ||= Faraday.new(provider.api_base) do |faraday|
28
28
  setup_timeout(faraday)
29
29
  setup_logging(faraday)
30
30
  setup_retry(faraday)
@@ -36,14 +36,14 @@ module RubyLLM
36
36
  def post(url, payload, &)
37
37
  body = payload.is_a?(Hash) ? JSON.generate(payload, ascii_only: false) : payload
38
38
  @connection.post url, body do |req|
39
- req.headers.merge! @provider.headers(@config) if @provider.respond_to?(:headers)
39
+ req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
40
40
  yield req if block_given?
41
41
  end
42
42
  end
43
43
 
44
44
  def get(url, &)
45
45
  @connection.get url do |req|
46
- req.headers.merge! @provider.headers(@config) if @provider.respond_to?(:headers)
46
+ req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
47
47
  yield req if block_given?
48
48
  end
49
49
  end
@@ -106,16 +106,21 @@ module RubyLLM
106
106
  end
107
107
 
108
108
  def ensure_configured!
109
- return if @provider.configured?(@config)
109
+ return if @provider.configured?
110
110
 
111
+ missing = @provider.configuration_requirements.reject { |req| @config.send(req) }
111
112
  config_block = <<~RUBY
112
113
  RubyLLM.configure do |config|
113
- #{@provider.missing_configs(@config).map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
114
+ #{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
114
115
  end
115
116
  RUBY
116
117
 
117
118
  raise ConfigurationError,
118
- "#{@provider.slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
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]
119
124
  end
120
125
  end
121
126
  end
@@ -22,9 +22,8 @@ module RubyLLM
22
22
  Image.paint(*args, **kwargs, context: self, &)
23
23
  end
24
24
 
25
- def connection_for(provider_module)
26
- slug = provider_module.slug.to_sym
27
- @connections[slug] ||= Connection.new(provider_module, @config)
25
+ def connection_for(provider_instance)
26
+ provider_instance.connection
28
27
  end
29
28
  end
30
29
  end
@@ -20,12 +20,11 @@ module RubyLLM
20
20
  dimensions: nil)
21
21
  config = context&.config || RubyLLM.config
22
22
  model ||= config.default_embedding_model
23
- model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
23
+ model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
24
+ config: config)
24
25
  model_id = model.id
25
26
 
26
- provider = Provider.for(model_id) if provider.nil?
27
- connection = context ? context.connection_for(provider) : provider.connection(config)
28
- provider.embed(text, model: model_id, connection:, dimensions:)
27
+ provider_instance.embed(text, model: model_id, dimensions:)
29
28
  end
30
29
  end
31
30
  end
@@ -40,9 +40,9 @@ module RubyLLM
40
40
  # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
41
41
  # Uses provider's parse_error method to extract meaningful error messages.
42
42
  class ErrorMiddleware < Faraday::Middleware
43
- def initialize(app, provider:)
43
+ def initialize(app, options = {})
44
44
  super(app)
45
- @provider = provider
45
+ @provider = options[:provider]
46
46
  end
47
47
 
48
48
  def call(env)
@@ -43,12 +43,11 @@ module RubyLLM
43
43
  context: nil)
44
44
  config = context&.config || RubyLLM.config
45
45
  model ||= config.default_image_model
46
- model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
46
+ model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
47
+ config: config)
47
48
  model_id = model.id
48
49
 
49
- provider = Provider.for(model_id) if provider.nil?
50
- connection = context ? context.connection_for(provider) : provider.connection(config)
51
- provider.paint(prompt, model: model_id, size:, connection:)
50
+ provider_instance.paint(prompt, model: model_id, size:)
52
51
  end
53
52
  end
54
53
  end
@@ -55,6 +55,10 @@ module RubyLLM
55
55
  }.compact
56
56
  end
57
57
 
58
+ def instance_variables
59
+ super - [:@raw]
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def normalize_content(content)