llm.rb 12.5.1 → 12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5eec18b65456b95a668fd5552fbd1c73464e26c55d07cc980efe5973e82fd548
4
- data.tar.gz: 4ab12f000f12f64e65afbca3ac4b08d79751f6d860c7cc9cab685c0e688d20c7
3
+ metadata.gz: 1660ea34a112d4c31eb8c9a51000ecd244b0f5629a4f763eceecd734d96f765b
4
+ data.tar.gz: 8040eefe13ffc48a40181642dad749d6abd7d16950f5d9e379ef006a0bb5af0c
5
5
  SHA512:
6
- metadata.gz: 86a7207e852129c3e22a747e74c8f5bcf12666886786104a38b01d2d80cc8cf82a008678fc7c5817203e138b6929aeeaba090af87f95fbea4185e7b733bbc8ff
7
- data.tar.gz: 40c2df5d31af69ff01c666259c2f39c54f545a9bb1115e4419266c2ce3a467467236ac3f2c9d39154d3242ebf9d12cb1541dad3c21f1b60290c1a816ac09003e
6
+ metadata.gz: cc1821c04eb0f996ca3e5d86d1722565e1fe2bc13351f3771c57de556d7ee9e97d930b3516ec70dea9686fad6443498c40d50584c6065a95242318f054a98d83
7
+ data.tar.gz: 3cc468ef1332c6f39d6b34a6a11fafe91eaf459ed80d5e79709f8106278fcb6fa1ba4f67b701a38539c3cba4d6967ad88858cccb4bb6b5b12690290036ad2afb
data/CHANGELOG.md CHANGED
@@ -15,6 +15,101 @@
15
15
 
16
16
  ## What's next
17
17
 
18
+ ## v12.6.0
19
+
20
+ Changes since `v12.5.1`.
21
+
22
+ This release adds bulk defaults for tools and agents — `LLM::Tool.defaults`
23
+ for setting parameter defaults and `LLM::Agent.set` for mass-assigning
24
+ class-level defaults, both mirrored on ActiveRecord and Sequel agent models.
25
+
26
+ It also makes `LLM::Interrupt` reliable across every concurrency strategy
27
+ (:thread, :call, :fiber, :task, :fork, and :ractor) so tool cancellation
28
+ works consistently regardless of execution backend, and fixes a stale fiber
29
+ reference in `LLM::Context#talk` that could prevent interruption after a
30
+ prior call.
31
+
32
+ ### Add
33
+
34
+ * **tool: add `defaults` method for setting parameter defaults** <br>
35
+ Add `LLM::Tool.defaults(properties)` for bulk-setting default values
36
+ on tool parameters, matching the same interface as `LLM::Schema.defaults`.
37
+ Each key maps to a parameter name; unknown keys raise `KeyError`.
38
+
39
+ * **agent: add `set` method for bulk-assigning class-level defaults** <br>
40
+ Add `LLM::Agent.set(properties)` for mass-assigning agent defaults
41
+ from a Hash. Each key maps to a class-level accessor; unknown keys
42
+ raise `KeyError`.
43
+
44
+ * **active_record: expose `set` on `acts_as_agent` models** <br>
45
+ ActiveRecord models using `acts_as_agent` can call `set` to
46
+ bulk-assign agent class-level defaults.
47
+
48
+ * **sequel: expose `set` on `plugin :agent` models** <br>
49
+ Sequel models using `plugin :agent` can call `set` to bulk-assign
50
+ agent class-level defaults.
51
+
52
+ ### Fix
53
+
54
+ * **function: raise `LLM::Interrupt` on thread where tool is running** <br>
55
+ On cancel, `LLM::Interrupt` is now raised on the thread that is
56
+ running a tool. The tool can rescue `LLM::Interrupt` and gracefully
57
+ terminate (e.g., clean up resources). The previous approach used
58
+ `Thread#interrupt` which was less reliable — it did not interrupt a
59
+ sleeping thread.
60
+
61
+ * **function: suppress thread exception reporting in `:thread` concurrency** <br>
62
+ Threads spawned by the `:thread` concurrency strategy now have
63
+ `report_on_exception` set to `false`, preventing noisy exception
64
+ messages from appearing on stderr when a thread is interrupted
65
+ during tool execution.
66
+
67
+ * **context: clear `@owner` after `talk` completes** <br>
68
+ `LLM::Context#talk` now clears the `@owner` reference in an
69
+ `ensure` block after the method completes, so `interrupt!` does
70
+ not attempt to interrupt a stale fiber reference from a prior
71
+ call.
72
+
73
+ * **function: raise `LLM::Interrupt` on thread waiting in `CallGroup#wait`** <br>
74
+ When using `ctx.wait(:call)`, `LLM::Interrupt` is now raised on the
75
+ thread executing the sequential tool wait. `CallGroup#wait` tracks the
76
+ active thread and `interrupt!` raises `LLM::Interrupt` on it, enabling
77
+ interruption of the `:call` concurrency strategy just like the existing
78
+ `:thread` strategy.
79
+
80
+ * **function: raise `LLM::Interrupt` on fiber-backed tool tasks** <br>
81
+ `LLM::Interrupt` is now raised on the active fiber via `Fiber#raise`
82
+ when interrupting `:fiber`-concurrency tools.
83
+ <br><br>
84
+ `Task#interrupt!` now dispatches by task type — `Thread#raise` for
85
+ threads, `Fiber#raise` for fibers — making interruption reliable
86
+ across all concurrency strategies.
87
+
88
+ * **function: raise `LLM::Interrupt` on fork-backed tool tasks** <br>
89
+ `LLM::Interrupt` is now raised on the main thread of a fork child
90
+ process via `Thread.main.raise(LLM::Interrupt)` when interrupting
91
+ `:fork`-concurrency tools, and the fork `Task#wait` re-raises the
92
+ interrupt on the parent side — making interruption reliable across
93
+ all concurrency strategies including `:fork`.
94
+
95
+ * **function: raise `LLM::Interrupt` on `Async::Task`-backed tool tasks** <br>
96
+ `LLM::Interrupt` is now raised on the underlying fiber of an
97
+ `Async::Task` via `Fiber#raise` when interrupting `:task`-concurrency
98
+ tools. `Task#interrupt!` now detects `Async::Task` instances and
99
+ dispatches to `Fiber#raise`, extending reliable interruption to the
100
+ `:task` concurrency strategy under the Async runtime.
101
+
102
+ * **function: raise `LLM::Interrupt` on ractor-backed tool tasks** <br>
103
+ `LLM::Interrupt` is now raised on the main thread inside a ractor
104
+ via `Thread.main.raise(LLM::Interrupt)` when interrupting
105
+ `:ractor`-concurrency tools. A listener thread inside the tool
106
+ ractor waits for an interrupt message via `Ractor.receive` and
107
+ raises `LLM::Interrupt` on the ractor's main thread.
108
+ <br><br>
109
+ `Task#interrupt!` delegates to the mailbox to send the interrupt
110
+ message — extending reliable interruption to the `:ractor`
111
+ concurrency strategy.
112
+
18
113
  ## v12.5.1
19
114
 
20
115
  Changes since `v12.5.0`.
data/README.md CHANGED
@@ -362,11 +362,10 @@ require "llm"
362
362
  require "llm/active_record"
363
363
 
364
364
  class Agent < ApplicationRecord
365
- acts_as_agent do |agent|
366
- agent.model "deepseek-v4-pro"
367
- agent.instructions "solve the user's query"
368
- agent.tools [Research, FinalizeResearch, ActOnResearch]
369
- end
365
+ acts_as_agent
366
+ set instructions: "solve the user's query",
367
+ model: "deepseek-v4-pro",
368
+ tools: [Research, FinalizeResearch, ActOnResearch]
370
369
 
371
370
  private
372
371
 
@@ -17,9 +17,41 @@ module LLM::ActiveRecord
17
17
  # class and forwarded to an internal agent subclass.
18
18
  module ActsAsAgent
19
19
  module ClassMethods
20
+ ##
21
+ # @return [Class<LLM::Agent>]
20
22
  def agent
21
23
  @agent ||= Class.new(LLM::Agent)
22
24
  end
25
+
26
+ ##
27
+ # Bulk-assign class-level agent defaults.
28
+ #
29
+ # Each key is resolved by calling the corresponding class method on the
30
+ # internal agent subclass.
31
+ #
32
+ # @example
33
+ # class Agent < ApplicationRecord
34
+ # acts_as_agent
35
+ # set instructions: "You are a system administrator",
36
+ # model: "gpt-4.1-nano",
37
+ # tools: [Shell]
38
+ # end
39
+ #
40
+ # @param [Hash] properties
41
+ # @option properties [String] :instructions
42
+ # @option properties [String] :model
43
+ # @option properties [Array<LLM::Function>] :tools
44
+ # @option properties [Array<String>] :skills
45
+ # @option properties [#to_json] :schema
46
+ # @option properties [Symbol, Array<Symbol>] :concurrency
47
+ # @option properties [LLM::Tracer, Proc] :tracer
48
+ # @option properties [Object, Proc] :stream
49
+ # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
50
+ # @raise [KeyError] when a property key does not match a class-level accessor
51
+ # @return [void]
52
+ def set(properties)
53
+ agent.set(properties)
54
+ end
23
55
  end
24
56
 
25
57
  module Hooks
data/lib/llm/agent.rb CHANGED
@@ -27,10 +27,10 @@ module LLM
27
27
  #
28
28
  # @example
29
29
  # class SystemAdmin < LLM::Agent
30
- # model "gpt-4.1-nano"
31
- # instructions "You are a Linux system admin"
32
- # tools Shell
33
- # schema Result
30
+ # set model: "gpt-4.1-nano",
31
+ # instructions: "You are a Linux system admin",
32
+ # tools: [Shell],
33
+ # schema: Result
34
34
  # end
35
35
  #
36
36
  # llm = LLM.openai(key: ENV["KEY"])
@@ -42,6 +42,42 @@ module LLM
42
42
  # @return [LLM::Provider]
43
43
  attr_reader :llm
44
44
 
45
+ ##
46
+ # Bulk-assign class-level agent defaults from a Hash.
47
+ #
48
+ # Each key is resolved by calling the corresponding class method on the
49
+ # agent subclass. An error is raised for unknown keys so that typos are
50
+ # caught early.
51
+ #
52
+ # @example
53
+ # class AdminAgent < LLM::Agent
54
+ # set instructions: "You are a system administrator",
55
+ # model: "gpt-4.1-nano",
56
+ # tools: [Shell, ReadFile]
57
+ # end
58
+ #
59
+ # @param [Hash] properties
60
+ # @option properties [String] :instructions
61
+ # @option properties [String] :model
62
+ # @option properties [Array<LLM::Function>] :tools
63
+ # @option properties [Array<String>] :skills
64
+ # @option properties [#to_json] :schema
65
+ # @option properties [Symbol, Array<Symbol>] :concurrency
66
+ # @option properties [LLM::Tracer, Proc] :tracer
67
+ # @option properties [Object, Proc] :stream
68
+ # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
69
+ # @raise [KeyError] when a property key does not match a class-level accessor
70
+ # @return [void]
71
+ def self.set(properties)
72
+ properties.each do
73
+ if respond_to?(_1)
74
+ public_send(_1, _2)
75
+ else
76
+ raise KeyError, "key not found: #{_1}"
77
+ end
78
+ end
79
+ end
80
+
45
81
  ##
46
82
  # Set or get the default model
47
83
  # @param [String, nil] model
data/lib/llm/context.rb CHANGED
@@ -206,6 +206,8 @@ module LLM
206
206
  @messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
207
207
  @messages.concat [res.choices[-1]].compact
208
208
  res
209
+ ensure
210
+ @owner = nil
209
211
  end
210
212
 
211
213
  ##
@@ -14,6 +14,7 @@ class LLM::Function
14
14
  # @return [LLM::Function::CallGroup]
15
15
  def initialize(functions)
16
16
  @functions = functions
17
+ @owner = nil
17
18
  end
18
19
 
19
20
  ##
@@ -25,6 +26,7 @@ class LLM::Function
25
26
  ##
26
27
  # @return [nil]
27
28
  def interrupt!
29
+ @owner&.raise(LLM::Interrupt)
28
30
  nil
29
31
  end
30
32
  alias_method :cancel!, :interrupt!
@@ -32,7 +34,10 @@ class LLM::Function
32
34
  ##
33
35
  # @return [Array<LLM::Function::Return>]
34
36
  def wait
37
+ @owner = Thread.current
35
38
  @functions.map(&:call)
39
+ ensure
40
+ @owner = nil
36
41
  end
37
42
  alias_method :value, :wait
38
43
  end
@@ -62,7 +62,7 @@ class LLM::Function
62
62
  ##
63
63
  # @return [nil]
64
64
  def interrupt!
65
- @fibers.each(&:interrupt!)
65
+ @fibers.each { _1.task.raise(LLM::Interrupt) if _1.task.alive? }
66
66
  nil
67
67
  end
68
68
  alias_method :cancel!, :interrupt!
@@ -24,6 +24,8 @@ class LLM::Function
24
24
  runner = @function.runner
25
25
  controller = setup(runner)
26
26
  @ch.result.write([:result, call!(runner)])
27
+ rescue LLM::Interrupt
28
+ @ch.result.write([:interrupt])
27
29
  rescue => ex
28
30
  @ch.result.write([:result, error(ex)])
29
31
  ensure
@@ -56,8 +58,7 @@ class LLM::Function
56
58
  ready << true
57
59
  kind = @ch.control.recv
58
60
  next unless kind == :interrupt
59
- hook = %i[on_cancel on_interrupt].find { runner.respond_to?(_1) }
60
- runner.public_send(hook) if hook
61
+ Thread.main.raise(LLM::Interrupt)
61
62
  rescue IOError, ArgumentError
62
63
  end
63
64
  ready.pop
@@ -52,6 +52,7 @@ class LLM::Function
52
52
  # @return [LLM::Function::Return]
53
53
  def wait
54
54
  kind, data = @ch.result.recv
55
+ raise LLM::Interrupt if kind == :interrupt
55
56
  raise ArgumentError, "Unknown fork message: #{kind.inspect}" unless kind == :result
56
57
  result = Return.new(data[:id], data[:name], data[:value])
57
58
  reap
@@ -35,22 +35,32 @@ class LLM::Function
35
35
  waiters = []
36
36
  loop do
37
37
  case ::Ractor.receive
38
- in [:done, *result]
38
+ in [:done, *data]
39
+ result ||= data
39
40
  done = true
40
41
  waiters.each { _1.send(result) }
41
42
  waiters.clear
42
43
  in [:alive?, reply]
43
44
  reply.send(!done)
44
45
  in [:wait, reply]
45
- done ? reply.send(result) : waiters << reply
46
+ done ? reply.send(result) : (waiters << reply)
47
+ in [:interrupt]
48
+ @tool&.send(:interrupt)
46
49
  end
47
50
  end
48
51
  end
49
52
 
50
53
  def spawn
51
- ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
54
+ @tool = ::Ractor.new(@mailbox, @runner_class, @id, @name, @arguments) do |mailbox, runner_class, id, name, arguments|
55
+ Thread.new do
56
+ ::Ractor.receive == :interrupt or next
57
+ Thread.main.raise(LLM::Interrupt)
58
+ rescue ::Ractor::Error
59
+ end
52
60
  kwargs = Hash === arguments ? arguments.transform_keys(&:to_sym) : arguments
53
61
  mailbox.send([:done, id, name, runner_class.new.call(**kwargs)])
62
+ rescue LLM::Interrupt
63
+ mailbox.send([:done, id, name, {cancelled: true, reason: "interrupted"}])
54
64
  rescue => ex
55
65
  mailbox.send([:done, id, name, {error: true, type: ex.class.name, message: ex.message}])
56
66
  end
@@ -28,6 +28,13 @@ class LLM::Function
28
28
  request(:wait)
29
29
  end
30
30
 
31
+ ##
32
+ # @return [nil]
33
+ def interrupt!
34
+ task.send([:interrupt])
35
+ nil
36
+ end
37
+
31
38
  private
32
39
 
33
40
  def request(type)
@@ -43,6 +43,7 @@ class LLM::Function
43
43
  ##
44
44
  # @return [nil]
45
45
  def interrupt!
46
+ mailbox&.interrupt!
46
47
  nil
47
48
  end
48
49
  alias_method :cancel!, :interrupt!
@@ -33,7 +33,18 @@ class LLM::Function
33
33
  ##
34
34
  # @return [nil]
35
35
  def interrupt!
36
- task.interrupt! if task.respond_to?(:interrupt!)
36
+ case task
37
+ when Thread
38
+ task.raise(LLM::Interrupt)
39
+ when Fiber
40
+ task.raise(LLM::Interrupt) if task.alive?
41
+ else
42
+ if defined?(::Async::Task) and ::Async::Task === task
43
+ task.fiber&.raise(LLM::Interrupt) if task.alive?
44
+ elsif task.respond_to?(:interrupt!)
45
+ task.interrupt!
46
+ end
47
+ end
37
48
  function&.interrupt!
38
49
  nil
39
50
  end
@@ -68,7 +68,7 @@ class LLM::Function
68
68
  ##
69
69
  # @return [nil]
70
70
  def interrupt!
71
- @threads.each(&:interrupt!)
71
+ @threads.each { _1.task.raise LLM::Interrupt }
72
72
  nil
73
73
  end
74
74
  alias_method :cancel!, :interrupt!
@@ -11,6 +11,8 @@ class LLM::Function
11
11
  result = super
12
12
  @tracer.on_tool_finish(result:, span:)
13
13
  result
14
+ rescue LLM::Interrupt
15
+ raise
14
16
  rescue => ex
15
17
  @tracer.on_tool_error(ex:, span:)
16
18
  raise(ex)
data/lib/llm/function.rb CHANGED
@@ -256,7 +256,7 @@ class LLM::Function
256
256
  LLM.require "async" unless defined?(::Async)
257
257
  Async { call! }
258
258
  when :thread
259
- Thread.new { call! }
259
+ Thread.new { call! }.tap { _1.report_on_exception = false }
260
260
  when :fiber
261
261
  raise ArgumentError, "Fiber concurrency requires Fiber.scheduler" unless Fiber.scheduler
262
262
  Fiber.schedule { call! }
@@ -382,6 +382,8 @@ class LLM::Function
382
382
  runner = self.runner
383
383
  kwargs = arguments.respond_to?(:to_h) ? arguments.to_h.transform_keys(&:to_sym) : arguments
384
384
  Return.new(id, name, runner.call(**kwargs))
385
+ rescue LLM::Interrupt
386
+ raise
385
387
  rescue => ex
386
388
  Return.new(id, name, {error: true, type: ex.class.name, message: ex.message})
387
389
  end
@@ -32,9 +32,41 @@ module LLM::Sequel
32
32
  @llm_agent_options || Agent::DEFAULTS
33
33
  end
34
34
 
35
+ ##
36
+ # @return [Class<LLM::Agent>]
35
37
  def agent
36
38
  @agent ||= Class.new(LLM::Agent)
37
39
  end
40
+
41
+ ##
42
+ # Bulk-assign class-level agent defaults.
43
+ #
44
+ # Each key is resolved by calling the corresponding class method on the
45
+ # internal agent subclass.
46
+ #
47
+ # @example
48
+ # class Agent < Sequel::Model
49
+ # plugin :llm_agent
50
+ # set instructions: "You are a system administrator",
51
+ # model: "gpt-4.1-nano",
52
+ # tools: [Shell]
53
+ # end
54
+ #
55
+ # @param [Hash] properties
56
+ # @option properties [String] :instructions
57
+ # @option properties [String] :model
58
+ # @option properties [Array<LLM::Function>] :tools
59
+ # @option properties [Array<String>] :skills
60
+ # @option properties [#to_json] :schema
61
+ # @option properties [Symbol, Array<Symbol>] :concurrency
62
+ # @option properties [LLM::Tracer, Proc] :tracer
63
+ # @option properties [Object, Proc] :stream
64
+ # @option properties [String, Symbol, Array<String, Symbol>, Proc] :confirm
65
+ # @raise [KeyError] when a property key does not match a class-level accessor
66
+ # @return [void]
67
+ def set(properties)
68
+ agent.set(properties)
69
+ end
38
70
  end
39
71
 
40
72
  module InstanceMethods
@@ -56,6 +56,18 @@ class LLM::Tool
56
56
  end
57
57
  end
58
58
 
59
+ ##
60
+ # Set default values for parameters.
61
+ # @param [Hash] defaults
62
+ # @return [LLM::Schema::Object]
63
+ def defaults(defaults)
64
+ lock do
65
+ function.params.tap do |schema|
66
+ defaults.each { Utils.fetch(schema.properties, _1).default(_2) }
67
+ end
68
+ end
69
+ end
70
+
59
71
  ##
60
72
  # @api private
61
73
  module Utils
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.5.1"
4
+ VERSION = "12.6.0"
5
5
  end
@@ -71,6 +71,7 @@ features that didn't make it into the homepage documentation.
71
71
  <summary>Cancellation</summary>
72
72
 
73
73
  - [Cancel a request](#cancel-a-request)
74
+ - [Tool interrupts](#tool-interrupts)
74
75
  </details>
75
76
 
76
77
  <details>
@@ -188,11 +189,11 @@ be simpler without it.
188
189
 
189
190
  ```ruby
190
191
  class Agent < LLM::Agent
191
- model "deepseek-v4-pro"
192
- tools { [DoResearch, FinalizeResearch, ActOnResearch] }
193
- stream { $stdout }
194
- tracer :set_tracer
195
- concurrency :fork
192
+ set model: "deepseek-v4-pro",
193
+ tools: [DoResearch, FinalizeResearch, ActOnResearch],
194
+ stream: -> { $stdout },
195
+ tracer: :set_tracer,
196
+ concurrency: :fork
196
197
 
197
198
  def research!
198
199
  talk "start the research"
@@ -828,6 +829,54 @@ rescue LLM::Interrupt
828
829
  end
829
830
  ```
830
831
 
832
+ #### Tool interrupts
833
+
834
+ When a running tool is interrupted &ndash; for example the user presses
835
+ ESC in the [REPL](#repl) &ndash; the runtime raises
836
+ [`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
837
+ on the tool's execution context. This behavior is uniform across
838
+ all six concurrency strategies (`:call`, `:thread`, `:fiber`,
839
+ `:task`, `:fork`, and `:ractor`).
840
+
841
+ A tool has two choices:
842
+
843
+ **Re-raise** `LLM::Interrupt` to cancel the entire turn. The
844
+ exception propagates out of the tool loop and the request is
845
+ aborted. This is the default when you don't rescue the exception.
846
+
847
+ ```ruby
848
+ def call
849
+ # ... do work ...
850
+ rescue LLM::Interrupt
851
+ cleanup
852
+ raise # cancel the turn
853
+ end
854
+ ```
855
+
856
+ **Return a value** to continue the tool loop. The model receives
857
+ the result and decides what to do next, aware that the tool was
858
+ interrupted.
859
+
860
+ ```ruby
861
+ def call
862
+ # ... do work ...
863
+ rescue LLM::Interrupt
864
+ cleanup
865
+ {ok: false, reason: "interrupted"} # continue the loop
866
+ end
867
+ ```
868
+
869
+ The right choice depends on the situation. A hard cancel aborts
870
+ the request outright &ndash; useful when continuing would produce
871
+ garbage. Returning a value lets the model adapt, which can be
872
+ helpful when the interrupt is temporary (e.g. a timeout).
873
+
874
+ The `:ractor` strategy is cooperative by nature &ndash; the ractor
875
+ must check for a cancel message at safe points in its execution.
876
+ The `:fork` strategy delivers the interrupt via a signal, which
877
+ interrupts blocking syscalls immediately. All other strategies
878
+ raise the exception directly on the executing thread or fiber.
879
+
831
880
  [Back to top](#table-of-contents)
832
881
 
833
882
  ## Tracer
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.5.1
4
+ version: 12.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - robert