llm.rb 12.1.0 → 12.2.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.
data/data/xai.json CHANGED
@@ -119,7 +119,7 @@
119
119
  "grok-4.3": {
120
120
  "id": "grok-4.3",
121
121
  "name": "Grok 4.3",
122
- "description": "xAI's default Grok for chat, coding, agentic tools, and lower hallucination risk",
122
+ "description": "xAI's Grok for chat, coding, agentic tools, and lower hallucination risk",
123
123
  "family": "grok",
124
124
  "attachment": true,
125
125
  "reasoning": true,
@@ -232,6 +232,65 @@
232
232
  "output": 0
233
233
  }
234
234
  },
235
+ "grok-4.5": {
236
+ "id": "grok-4.5",
237
+ "name": "Grok 4.5",
238
+ "description": "xAI's latest Grok for chat, coding, agentic tools, and lower hallucination risk",
239
+ "family": "grok",
240
+ "attachment": true,
241
+ "reasoning": true,
242
+ "reasoning_options": [
243
+ {
244
+ "type": "effort",
245
+ "values": [
246
+ "low",
247
+ "medium",
248
+ "high"
249
+ ]
250
+ }
251
+ ],
252
+ "tool_call": true,
253
+ "structured_output": true,
254
+ "temperature": true,
255
+ "release_date": "2026-07-08",
256
+ "last_updated": "2026-07-08",
257
+ "modalities": {
258
+ "input": [
259
+ "text",
260
+ "image",
261
+ "pdf"
262
+ ],
263
+ "output": [
264
+ "text"
265
+ ]
266
+ },
267
+ "open_weights": false,
268
+ "limit": {
269
+ "context": 500000,
270
+ "output": 500000
271
+ },
272
+ "cost": {
273
+ "input": 2,
274
+ "output": 6,
275
+ "cache_read": 0.5,
276
+ "tiers": [
277
+ {
278
+ "input": 4,
279
+ "output": 12,
280
+ "cache_read": 1,
281
+ "tier": {
282
+ "type": "context",
283
+ "size": 200000
284
+ }
285
+ }
286
+ ],
287
+ "context_over_200k": {
288
+ "input": 4,
289
+ "output": 12,
290
+ "cache_read": 1
291
+ }
292
+ }
293
+ },
235
294
  "grok-4.20-0309-reasoning": {
236
295
  "id": "grok-4.20-0309-reasoning",
237
296
  "name": "Grok 4.20 (Reasoning)",
data/lib/llm/agent.rb CHANGED
@@ -393,10 +393,26 @@ module LLM
393
393
  # with the agent and its current state. This
394
394
  # method requires the 'curses' gem to be installed
395
395
  # and available to require.
396
+ #
397
+ # @note
398
+ # By default this method disables the tracer for
399
+ # the duration of the repl session, and restores
400
+ # it afterwards.
401
+ # @param [Boolean] trace
402
+ # When true, the tracer is kept alive during the
403
+ # repl session. Default is false.
396
404
  # @return [void]
397
- def repl
405
+ def repl(trace: false)
406
+ if !trace
407
+ previous = tracer
408
+ self.tracer = nil
409
+ end
398
410
  require_relative "repl" unless defined?(::LLM::Repl)
399
411
  LLM::Repl.new(self).start
412
+ ensure
413
+ if !trace
414
+ self.tracer = previous
415
+ end
400
416
  end
401
417
 
402
418
  ##
data/lib/llm/context.rb CHANGED
@@ -426,7 +426,7 @@ module LLM
426
426
  # A tracer, or nil.
427
427
  # @return [void]
428
428
  def tracer=(other)
429
- @llm.tracer = nil
429
+ @llm.tracer = other || LLM::Tracer::Null.new(@llm)
430
430
  end
431
431
 
432
432
  ##
@@ -604,7 +604,7 @@ module LLM
604
604
  def repair!(messages, prompt)
605
605
  message = messages.last
606
606
  return unless message&.tool_call?
607
- returns = self.returns + [*prompt].grep(LLM::Function::Return)
607
+ returns = [self.returns, prompt].flatten.grep(LLM::Function::Return)
608
608
  cancelled = []
609
609
  [*message.extra.tool_calls].each do |tool|
610
610
  next if returns.any? { _1.id == tool[:id] }
data/lib/llm/function.rb CHANGED
@@ -390,6 +390,12 @@ class LLM::Function
390
390
  type: "function", name: @name, description: @description,
391
391
  parameters: (@params || {type: "object", properties: {}}).to_h.merge(additionalProperties: false), strict: false
392
392
  }.compact
393
+ when "LLM::Mistral"
394
+ params = @params || {type: "object", properties: {}}
395
+ {
396
+ type: "function",
397
+ function: {name: @name, description: @description, parameters: params}
398
+ }.compact
393
399
  else
394
400
  params = @params || {type: "object", properties: {}}
395
401
  {
data/lib/llm/provider.rb CHANGED
@@ -26,7 +26,7 @@ class LLM::Provider
26
26
  # Requires the net-http-persistent gem.
27
27
  # @param [LLM::Transport, Class, nil] transport
28
28
  # Optional override with any {LLM::Transport} instance or subclass.
29
- def initialize(key:, host:, port: 443, timeout: 60, ssl: true, base_path: "", persistent: false, transport: nil)
29
+ def initialize(key:, host:, port: 443, timeout: 180, ssl: true, base_path: "", persistent: false, transport: nil)
30
30
  @key = key
31
31
  @host = host
32
32
  @port = port
@@ -303,7 +303,7 @@ class LLM::Provider
303
303
  def with_tracer(tracer)
304
304
  had_override = weakmap.key?(self)
305
305
  previous = weakmap[self]
306
- weakmap[self] = tracer
306
+ weakmap[self] = tracer || LLM::Tracer::Null.new(self)
307
307
  yield
308
308
  ensure
309
309
  if had_override
@@ -92,10 +92,10 @@ module LLM
92
92
 
93
93
  ##
94
94
  # Returns the default model for chat completions
95
- # @see https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table claude-sonnet-4-20250514
95
+ # @see https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table claude-opus-4-8
96
96
  # @return [String]
97
97
  def default_model
98
- "claude-sonnet-4-20250514"
98
+ "claude-opus-4-8"
99
99
  end
100
100
 
101
101
  ##
@@ -194,9 +194,11 @@ module LLM
194
194
  end
195
195
 
196
196
  def normalize_complete_params(params)
197
+ except = %i[role model messages stream]
197
198
  params = {role: :user, model: default_model}.merge!(params)
198
199
  tools = resolve_tools(params.delete(:tools))
199
- params = [params, adapt_generation_config(params), adapt_tools(tools)].inject({}, &:merge!).compact
200
+ config = adapt_generation_config(params.except(*except))
201
+ params = [params.except(:schema), config, adapt_tools(tools)].inject({}, &:merge!).compact
200
202
  role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
201
203
  [params, stream, tools, role, model]
202
204
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::Mistral::RequestAdapter
4
+ ##
5
+ # @private
6
+ class Completion
7
+ ##
8
+ # @param [LLM::Message, Hash] message
9
+ # The message to format
10
+ def initialize(message)
11
+ @message = message
12
+ end
13
+
14
+ ##
15
+ # Adapts the message for the Mistral chat completions API
16
+ # @return [Hash]
17
+ def adapt
18
+ catch(:abort) do
19
+ if Hash === message
20
+ {role: message[:role], content: adapt_content(message[:content])}
21
+ elsif message.tool_call?
22
+ {role: message.role, content: nil, tool_calls: message.extra[:original_tool_calls]}
23
+ else
24
+ adapt_message
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def adapt_message
32
+ case content
33
+ when Array
34
+ adapt_array
35
+ else
36
+ {role: message.role, content: adapt_content(content)}
37
+ end
38
+ end
39
+
40
+ def adapt_array
41
+ if content.empty?
42
+ nil
43
+ elsif returns.any?
44
+ returns.map do
45
+ {
46
+ role: "tool",
47
+ name: _1.name,
48
+ tool_call_id: _1.id,
49
+ content: LLM.json.dump(_1.value)
50
+ }
51
+ end
52
+ else
53
+ {role: message.role, content: content.flat_map { adapt_content(_1) }}
54
+ end
55
+ end
56
+
57
+ def adapt_content(content)
58
+ case content
59
+ when LLM::Object
60
+ adapt_object(content)
61
+ when String
62
+ [{type: :text, text: content.to_s}]
63
+ when LLM::Response
64
+ adapt_remote_file(content)
65
+ when LLM::Message
66
+ adapt_content(content.content)
67
+ when LLM::Function::Return
68
+ throw(:abort, {
69
+ role: "tool",
70
+ name: content.name,
71
+ tool_call_id: content.id,
72
+ content: LLM.json.dump(content.value)
73
+ })
74
+ else
75
+ prompt_error!(content)
76
+ end
77
+ end
78
+
79
+ def adapt_object(object)
80
+ case object.kind
81
+ when :image_url
82
+ [{type: :image_url, image_url: {url: object.value}}]
83
+ when :local_file
84
+ adapt_local_file(object.value)
85
+ when :remote_file
86
+ adapt_remote_file(object.value)
87
+ else
88
+ prompt_error!(object)
89
+ end
90
+ end
91
+
92
+ def adapt_local_file(file)
93
+ if file.image?
94
+ [{type: :image_url, image_url: {url: file.to_data_uri}}]
95
+ else
96
+ [{type: :file, file: {filename: file.basename, file_data: file.to_data_uri}}]
97
+ end
98
+ end
99
+
100
+ def adapt_remote_file(file)
101
+ if file.file?
102
+ [{type: :file, file: {file_id: file.id}}]
103
+ else
104
+ prompt_error!(file)
105
+ end
106
+ end
107
+
108
+ def prompt_error!(content)
109
+ if LLM::Object === content
110
+ raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
111
+ "supported by the Mistral chat completions API."
112
+ else
113
+ raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
114
+ "is not supported by the Mistral chat completions API."
115
+ end
116
+ end
117
+
118
+ def message = @message
119
+ def content = message.content
120
+ def returns = content.grep(LLM::Function::Return)
121
+ end
122
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Mistral
4
+ ##
5
+ # @private
6
+ module RequestAdapter
7
+ require_relative "request_adapter/completion"
8
+ include LLM::OpenAI::RequestAdapter
9
+
10
+ ##
11
+ # @param [Array<LLM::Message>] messages
12
+ # The messages to adapt
13
+ # @return [Array<Hash>]
14
+ def adapt(messages, mode: nil)
15
+ messages.filter_map do |message|
16
+ Completion.new(message).adapt
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openai" unless defined?(LLM::OpenAI)
4
+
5
+ module LLM
6
+ ##
7
+ # The Mistral class implements a provider for
8
+ # [Mistral](https://mistral.ai) through its
9
+ # OpenAI-compatible API.
10
+ #
11
+ # @example
12
+ # #!/usr/bin/env ruby
13
+ # require "llm"
14
+ #
15
+ # llm = LLM.mistral(key: ENV["KEY"])
16
+ # ctx = LLM::Context.new(llm)
17
+ # ctx.talk "Hello"
18
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
19
+ class Mistral < OpenAI
20
+ require_relative "mistral/request_adapter"
21
+ include Mistral::RequestAdapter
22
+
23
+ HOST = "api.mistral.ai"
24
+ BASE_PATH = "/v1/"
25
+
26
+ ##
27
+ # @param key (see LLM::Provider#initialize)
28
+ # @param host (see LLM::Provider#initialize)
29
+ # @param base_path (see LLM::Provider#initialize)
30
+ # @return [LLM::Mistral]
31
+ def initialize(host: HOST, base_path: BASE_PATH, **)
32
+ super
33
+ end
34
+
35
+ ##
36
+ # @return [Symbol]
37
+ # Returns the provider's name
38
+ def name
39
+ :mistral
40
+ end
41
+
42
+ ##
43
+ # @return [NotImplementedError]
44
+ def images
45
+ raise NotImplementedError
46
+ end
47
+
48
+ ##
49
+ # Provides an embedding.
50
+ # @param input (see LLM::Provider#embed)
51
+ # @param model (see LLM::Provider#embed)
52
+ # @param params (see LLM::Provider#embed)
53
+ # @raise (see LLM::Provider#request)
54
+ # @return (see LLM::Provider#embed)
55
+ def embed(input, model: "mistral-embed", **params)
56
+ super
57
+ end
58
+
59
+ ##
60
+ # @raise [NotImplementedError]
61
+ def responses
62
+ raise NotImplementedError
63
+ end
64
+
65
+ ##
66
+ # @return [LLM::Mistral::Audio]
67
+ def audio
68
+ raise NotImplementedError
69
+ end
70
+
71
+ ##
72
+ # @raise [NotImplementedError]
73
+ def files
74
+ raise NotImplementedError
75
+ end
76
+
77
+ ##
78
+ # @raise [NotImplementedError]
79
+ def moderations
80
+ raise NotImplementedError
81
+ end
82
+
83
+ ##
84
+ # @raise [NotImplementedError]
85
+ def vector_stores
86
+ raise NotImplementedError
87
+ end
88
+
89
+ ##
90
+ # Returns the default model for chat completions
91
+ # @return [String]
92
+ def default_model
93
+ "mistral-large-latest"
94
+ end
95
+ end
96
+ end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "12.1.0"
4
+ VERSION = "12.2.0"
5
5
  end
data/lib/llm.rb CHANGED
@@ -180,6 +180,15 @@ module LLM
180
180
  LLM::XAI.new(**)
181
181
  end
182
182
 
183
+ ##
184
+ # @param key (see LLM::Mistral#initialize)
185
+ # @param host (see LLM::Mistral#initialize)
186
+ # @return (see LLM::Mistral#initialize)
187
+ def mistral(**)
188
+ lock(:require) { require_relative "llm/providers/mistral" unless defined?(LLM::Mistral) }
189
+ LLM::Mistral.new(**)
190
+ end
191
+
183
192
  ##
184
193
  # @param key (see LLM::ZAI#initialize)
185
194
  # @param host (see LLM::ZAI#initialize)
data/llm.gemspec CHANGED
@@ -5,7 +5,7 @@ require_relative "lib/llm/version"
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "llm.rb"
7
7
  spec.version = LLM::VERSION
8
- spec.authors = ["bsdrobert", "Antar Azri", "Rodrigo Serrano"]
8
+ spec.authors = ["robert", "Antar Azri", "Rodrigo Serrano"]
9
9
  spec.email = ["robert@r.uby.dev"]
10
10
 
11
11
  spec.summary = "Ruby's capable AI runtime"
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.1.0
4
+ version: 12.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - bsdrobert
7
+ - robert
8
8
  - Antar Azri
9
9
  - Rodrigo Serrano
10
10
  bindir: bin
@@ -293,6 +293,7 @@ files:
293
293
  - data/deepinfra.json
294
294
  - data/deepseek.json
295
295
  - data/google.json
296
+ - data/mistral.json
296
297
  - data/openai.json
297
298
  - data/xai.json
298
299
  - data/zai.json
@@ -419,6 +420,9 @@ files:
419
420
  - lib/llm/providers/google/stream_parser.rb
420
421
  - lib/llm/providers/google/utils.rb
421
422
  - lib/llm/providers/llamacpp.rb
423
+ - lib/llm/providers/mistral.rb
424
+ - lib/llm/providers/mistral/request_adapter.rb
425
+ - lib/llm/providers/mistral/request_adapter/completion.rb
422
426
  - lib/llm/providers/ollama.rb
423
427
  - lib/llm/providers/ollama/error_handler.rb
424
428
  - lib/llm/providers/ollama/models.rb