llm.rb 16.0.0.pre.10 → 17.0.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +524 -2
  3. data/README.md +857 -501
  4. data/data/alibaba.json +2 -2
  5. data/data/anthropic.json +66 -3
  6. data/data/bedrock.json +781 -12
  7. data/data/deepinfra.json +157 -1
  8. data/data/google.json +48 -0
  9. data/data/mistral.json +326 -16
  10. data/data/moonshot.json +2 -1
  11. data/data/openai.json +13 -0
  12. data/data/openrouter.json +1078 -1235
  13. data/data/xai.json +4 -2
  14. data/docs/deepdive/advanced/cancellation.md +35 -7
  15. data/docs/deepdive/advanced/compaction.md +5 -5
  16. data/docs/deepdive/advanced/context.md +45 -24
  17. data/docs/deepdive/advanced/{guard.md → intercept.md} +87 -83
  18. data/docs/deepdive/features/builtin_tools.md +1 -1
  19. data/docs/deepdive/features/concurrency.md +15 -10
  20. data/docs/deepdive/features/database.md +19 -19
  21. data/docs/deepdive/features/embeddings.md +4 -4
  22. data/docs/deepdive/features/files.md +7 -5
  23. data/docs/deepdive/fundamentals/agents.md +13 -8
  24. data/docs/deepdive/fundamentals/providers.md +1 -1
  25. data/docs/deepdive/fundamentals/schema.md +2 -2
  26. data/docs/deepdive/fundamentals/stream.md +8 -8
  27. data/docs/deepdive/fundamentals/tools.md +14 -14
  28. data/docs/deepdive/media/ocr.md +1 -1
  29. data/docs/deepdive/protocols/a2a.md +3 -3
  30. data/docs/deepdive/protocols/mcp.md +116 -7
  31. data/docs/deepdive/reference/cost.md +1 -1
  32. data/docs/deepdive/reference/response.md +1 -1
  33. data/docs/deepdive/reference/tracer.md +90 -22
  34. data/docs/deepdive.md +1 -1
  35. data/lib/llm/agent/interrupt.rb +35 -46
  36. data/lib/llm/agent/registry.rb +106 -0
  37. data/lib/llm/agent.rb +244 -269
  38. data/lib/llm/context.rb +63 -55
  39. data/lib/llm/function/array.rb +2 -1
  40. data/lib/llm/function/async/group.rb +2 -2
  41. data/lib/llm/function/async/task.rb +25 -48
  42. data/lib/llm/function/fiber/group.rb +2 -2
  43. data/lib/llm/function/fiber/task.rb +16 -30
  44. data/lib/llm/function/fork/group.rb +2 -2
  45. data/lib/llm/function/fork/job.rb +8 -4
  46. data/lib/llm/function/fork/task.rb +25 -30
  47. data/lib/llm/function/group.rb +33 -5
  48. data/lib/llm/function/ractor/group.rb +2 -2
  49. data/lib/llm/function/ractor/job.rb +8 -6
  50. data/lib/llm/function/ractor/task.rb +32 -9
  51. data/lib/llm/function/sequential/group.rb +2 -1
  52. data/lib/llm/function/sequential/task.rb +8 -1
  53. data/lib/llm/function/task.rb +52 -4
  54. data/lib/llm/function/thread/group.rb +2 -2
  55. data/lib/llm/function/thread/task.rb +36 -10
  56. data/lib/llm/function/window.rb +16 -0
  57. data/lib/llm/function.rb +14 -43
  58. data/lib/llm/intercept/multi.rb +40 -0
  59. data/lib/llm/intercept/null.rb +18 -0
  60. data/lib/llm/intercept.rb +96 -0
  61. data/lib/llm/message.rb +1 -1
  62. data/lib/llm/provider.rb +16 -1
  63. data/lib/llm/providers/anthropic/stream_parser.rb +1 -1
  64. data/lib/llm/providers/bedrock/stream_parser.rb +1 -1
  65. data/lib/llm/providers/deepseek/files.rb +49 -0
  66. data/lib/llm/providers/deepseek/request_adapter/completion.rb +15 -13
  67. data/lib/llm/providers/deepseek.rb +16 -2
  68. data/lib/llm/providers/google/stream_parser.rb +1 -1
  69. data/lib/llm/providers/openai/responses/stream_parser.rb +1 -1
  70. data/lib/llm/providers/openai/stream_parser.rb +1 -1
  71. data/lib/llm/stream.rb +8 -7
  72. data/lib/llm/tool.rb +1 -1
  73. data/lib/llm/tools/utils.rb +1 -1
  74. data/lib/llm/tracer/logger.rb +42 -1
  75. data/lib/llm/tracer/multi.rb +144 -0
  76. data/lib/llm/tracer/null.rb +20 -0
  77. data/lib/llm/tracer/pretty_logger.rb +33 -2
  78. data/lib/llm/tracer/rescue.rb +110 -0
  79. data/lib/llm/tracer/telemetry.rb +69 -4
  80. data/lib/llm/tracer.rb +125 -48
  81. data/lib/llm/utils.rb +12 -2
  82. data/lib/llm/version.rb +1 -1
  83. data/lib/llm.rb +36 -1
  84. data/llm.gemspec +14 -7
  85. metadata +21 -38
  86. data/lib/llm/function/tracing.rb +0 -21
  87. data/lib/llm/guard/null.rb +0 -19
  88. data/lib/llm/guard.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10b3b432390fb55cf12c843da1724138159859623d363b7c551b7887deaf22e2
4
- data.tar.gz: 0520f17de9070b675fbda52b50a1dc2dddba478519ce116ba318c0036dda1e3c
3
+ metadata.gz: '058dd39edfa1c7dee3a429e32c0af2f1ad022fbbfd4c5341be2116264027a09f'
4
+ data.tar.gz: 460e7aca153eff0138e53ca21e7b1063294869983676562c41f31a076cd74d91
5
5
  SHA512:
6
- metadata.gz: 892f5b9f20895252486f02b1783c9be7f8292703747be1f6bb46025e03b446774166795cd47bf24e345789cfe26e51c765b70fec0d647d6932d877f52bf89d7b
7
- data.tar.gz: 95cd84c98f4aed756b5ed4eaec4c096b9d04a7fe2fb4e35cd3edbe12ea63d9ab8dd55ff0d16863c290a2361729c7fd7a8c7557d48424b394c269ad5afe5a834e
6
+ metadata.gz: 9f9edd9b05c9825e5c9854fb113d98fd7e76bf53e992fd26ffb83e4021ac4a64902246389f2620de23901a939890ad2601366dead580036c64c37dad4ab92d62
7
+ data.tar.gz: 5b34de772aa2c6c1823e5bde6b2a8314ff4de120edbe1a1a64cd3be8b9c0b4f448806a8630ff79bfec39acc200e3c2a35720cd59ae95fb0aae82f0850878fc79
data/CHANGELOG.md CHANGED
@@ -1,11 +1,533 @@
1
1
  > Changelog <br>
2
2
  > [r.uby.dev](https://r.uby.dev) project
3
3
 
4
- This file covers the v16 series, which has not been released yet. Releases up to
5
- and including v15 are kept in [changelog/old.md](changelog/old.md).
4
+ This file covers the v17 series and the previous
5
+ v16 series. <br> Previous releases (v15 and
6
+ before) can be seen in [changelog/old.md](changelog/old.md).
6
7
 
7
8
  ## What's next
8
9
 
10
+ *No unreleased changes yet. Check back after the next release.*
11
+
12
+ ## v17.0.0
13
+
14
+ Changes since `v16.0.0`.
15
+
16
+ This release is large, and includes a number of breaking
17
+ changes. It covers a lot of ground. I will try to cover the
18
+ highlights but I won't be able to cover everything, and
19
+ there won't be much depth. Check out the changelog entries
20
+ to learn more.
21
+
22
+ The Files API provided by deepseek is now supported
23
+ (via [LLM::DeepSeek#files](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek/Files.html)),
24
+ and there was a large focus on the cancellation and
25
+ interrupt story. [LLM::Tracer]((https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
26
+ saw a lot of changes and it has the most changelog
27
+ entries in this release.
28
+
29
+ The `LLM::Guard` class was a focus as well, and it
30
+ has been renamed to
31
+ [LLM::Intercept](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
32
+ . An intercept can block a tool call with a return
33
+ of its own, it can mutate tool-call parameters in
34
+ place, validate tool call parameters, and anything
35
+ else a middleman that sits between the model and a
36
+ tool could do.
37
+
38
+ It is now possible to combine multiple tracers _or_
39
+ intercepts into a single tracer, or intercept. The
40
+ [LLM::Tracer.multi]((https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method))
41
+ and
42
+ [LLM::Intercept.multi]((https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#multi-class_method))
43
+ methods exist for this purpose.
44
+
45
+ For everything else, keep reading.
46
+
47
+ ### Breaking
48
+
49
+ #### Migration
50
+
51
+ | Old | New |
52
+ |-----|-----|
53
+ | the default read timeout is 600 seconds | the default read timeout is 240 seconds |
54
+ | `LLM::Tracer#on_interrupt` and `#on_exit` do nothing | both raise `NotImplementedError`, so a tracer answers for each |
55
+ | `LLM::Tracer#on_tool_finish(result:, span:)` and `#on_tool_error(ex:, span:)` | both also take `started_at:` and `finished_at:` |
56
+ | `LLM::Tracer#on_tool_error` is given the `Exception` a tool raised | it is given the `LLM::Function::Return` that holds the error |
57
+ | a tracer's tool callbacks run on the thread or fiber that ran the tool | they run on the agent's thread, like the other callbacks |
58
+ | a cancelled `:ractor` call answers in band with `cancelled: true` | it raises `LLM::Interrupt` in the caller's thread |
59
+ | `LLM::Guard`, `LLM::Guard::Null` | `LLM::Intercept`, `LLM::Intercept::Null` |
60
+ | `guard:` and `guard_options:` on a context or agent | `intercept:` and `intercept_options:` |
61
+ | `LLM::Context#guard` | `LLM::Context#intercept` |
62
+ | a guard that returned anything truthy stopped the tool call | only a `LLM::Function::Return` stops it, and any other value lets the call run |
63
+ | `trace:` on `LLM::Agent#console`, a deprecated alias | it is removed, and `tracer:` is the keyword |
64
+
65
+ ### Intercept
66
+
67
+ * **intercept: rename `LLM::Guard` to `LLM::Intercept`** <br>
68
+ The hook that sees a pending tool call before it
69
+ runs is an
70
+ [`LLM::Intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html)
71
+ now, because it does more than guard a call:
72
+ `#call` receives the `LLM::Function`, so an
73
+ intercept can rewrite a call and let it run by
74
+ returning `nil`. A subclass of `LLM::Guard` is
75
+ renamed, the `guard:` and `guard_options:` a
76
+ context or agent takes are `intercept:` and
77
+ `intercept_options:`, and `LLM::Context#guard` is
78
+ `LLM::Context#intercept`. `LLM::Guard` is
79
+ removed, and no alias is left for it.
80
+
81
+ * **intercept: add `LLM::Intercept.multi`** <br>
82
+ The
83
+ [`LLM::Intercept.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#multi-class_method)
84
+ factory combines one or more intercepts into a
85
+ single
86
+ [`LLM::Intercept::Multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept/Multi.html)
87
+ that a context or agent can be given as
88
+ `intercept:`. Each intercept is called in the
89
+ order it was given, and the first that returns an
90
+ [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html)
91
+ ends the call, so an intercept that returns `nil`
92
+ lets the next one decide. An `intercept:` is
93
+ taken as an instance now as well as a class,
94
+ which is what lets the factory's return value be
95
+ used as it is.
96
+
97
+ * **intercept: give an intercept the options it was built with** <br>
98
+ The `intercept_options:` a context or agent is
99
+ built with now reach
100
+ [`LLM::Intercept#call`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#call-instance_method)
101
+ as keywords, and an intercept can read them
102
+ through
103
+ [`LLM::Intercept#options`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#options-instance_method).
104
+ Before, the options were held on the context and
105
+ never passed to the intercept, so a subclass that
106
+ wanted them had no way to read them.
107
+
108
+ ### Provider
109
+
110
+ * **deepseek: upload a file once and refer to it by id** <br>
111
+ [`LLM::DeepSeek#files`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek.html#files-instance_method)
112
+ returns an
113
+ [`LLM::DeepSeek::Files`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek/Files.html)
114
+ object now, so a file can be uploaded once and a
115
+ later chat request can name it instead of
116
+ resending its bytes. Before, the call raised
117
+ `NotImplementedError`. Uploads default to
118
+ `purpose: "user_data"`, the only purpose
119
+ DeepSeek accepts, and requests go to the root of
120
+ the API host, where DeepSeek serves its Files
121
+ API.
122
+
123
+ * **provider: add `LLM::Provider#retry_errors`** <br>
124
+ The
125
+ [`LLM::Provider#retry_errors`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#retry_errors-instance_method)
126
+ method returns the exceptions that a request can
127
+ raise and that are retried up to the provider's
128
+ retry budget. A provider extends the array it
129
+ inherits rather than replacing it, and
130
+ [`LLM::DeepSeek#retry_errors`](https://r.uby.dev/api-docs/llm.rb/LLM/DeepSeek.html#retry_errors-instance_method)
131
+ adds `LLM::ServerError`, the error DeepSeek
132
+ returns when its servers are overloaded, so a
133
+ request that finds an overloaded server is retried
134
+ with a growing delay like a rate limit instead of
135
+ failing on the first response.
136
+
137
+ * **provider: drop the default read timeout to 240 seconds** <br>
138
+ The default read timeout is now 240 seconds (four
139
+ minutes), down from 600 seconds (ten minutes). A
140
+ timeout is retried, so the old default could be
141
+ spent on a response that never arrives, and the
142
+ new one fails sooner and opens a new session
143
+ instead. DeepSeek prompted the change: it can
144
+ stall past four minutes when it is under heavy
145
+ load. Pass `read_timeout:` or `timeout:` to a
146
+ provider to choose your own.
147
+
148
+ ### Agent
149
+
150
+ * **agent: interrupt an agent via `LLM::Agent#id`** <br>
151
+ The
152
+ [`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
153
+ method can interrupt an agent with nothing more
154
+ than an ID that references it. A controller, a
155
+ background job, or a socket handler can use this
156
+ method to interrupt an agent that is running
157
+ in the same process but on a different thread
158
+ or fiber.
159
+
160
+ * **agent: add `LLM::Agent.registry`** <br>
161
+ The
162
+ [`LLM::Agent.registry`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#registry-class_method)
163
+ method returns an object that holds every
164
+ active instance of
165
+ [`LLM::Agent`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
166
+ An agent enters the registry when it starts a turn,
167
+ and it exits the registry after that. The registry is
168
+ thread-safe, local to a single process
169
+ and
170
+ [`LLM.interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM.html#interrupt-class_method)
171
+ uses it under the hood to find an agent by ID.
172
+
173
+ * **agent: `LLM.interrupt` can retry until it finds an agent** <br>
174
+ The method now takes `attempts:` and `interval:`, so
175
+ a caller that asks for an agent before its turn has
176
+ started can poll for it instead of giving up on the
177
+ first miss, which is the loop a caller used to write
178
+ by hand. `attempts` defaults to 1 and `interval` to
179
+ 0.1 seconds, so the method is unchanged until a caller
180
+ asks it to wait. It returns `true` when an interrupt
181
+ took place, and `false` otherwise.
182
+
183
+ * **agent: set an intercept at the class level** <br>
184
+ [`LLM::Agent.intercept`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#intercept-class_method)
185
+ is where an agent's intercept is declared, so it
186
+ can sit beside the agent's other defaults. It
187
+ takes an intercept, or a block that is resolved
188
+ when the agent is built, and `set intercept:`
189
+ takes a `Symbol` that names an instance method,
190
+ which is how
191
+ [`LLM::Intercept.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Intercept.html#multi-class_method)
192
+ is composed. An `intercept:` given to
193
+ [`LLM::Agent.new`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#initialize-instance_method)
194
+ is resolved the same way, and an ActiveRecord or
195
+ Sequel model that wraps an agent carries it into
196
+ the context it builds.
197
+
198
+ * **agent: remove the deprecated `trace:` keyword from `LLM::Agent#console`** <br>
199
+ The `trace:` keyword is gone from
200
+ [`LLM::Agent#console`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#console-instance_method)
201
+ and from its `repl` alias. It was renamed to
202
+ `tracer:` in an earlier release, and it was kept
203
+ for a caller that still passed it, with a warning
204
+ each time. The keyword is `tracer:`, so
205
+ `trace: true` is `tracer: true`.
206
+
207
+ ### Function
208
+
209
+ * **function: `:ractor` raises `LLM::Interrupt` (like everyone else)** <br>
210
+ An
211
+ [`LLM::Interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Interrupt.html)
212
+ that is delivered to a tool running on a ractor
213
+ will travel back to the caller and raise itself
214
+ on the caller's thread, the same as all other
215
+ concurrency strategies. This only happens after
216
+ all tools have been interrupted themselves, and
217
+ it is the last step in an interrupt. The caller
218
+ is expected to rescue `LLM::Interrupt` and handle
219
+ the interrupt from there.
220
+
221
+ * **function: add `LLM::Function::Task#intercepted?`** <br>
222
+ An intercept stops a tool call when it returns
223
+ an
224
+ [`LLM::Function::Return`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Return.html),
225
+ because that Return is the answer the model is
226
+ given for the call. Any other value lets the call
227
+ through, where only `nil` did that before, so a
228
+ guard that returned anything else stopped a tool
229
+ call and answered the model with it.
230
+ [`LLM::Function::Task#intercepted?`](https://r.uby.dev/api-docs/llm.rb/LLM/Function/Task.html#intercepted?-instance_method)
231
+ is the predicate a task asks, and it is true only
232
+ for a Return.
233
+
234
+ ### Tracer
235
+
236
+ * **tracer: add `LLM::Tracer::Multi`** <br>
237
+ The
238
+ [`LLM::Tracer::Multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Multi.html)
239
+ tracer fans every tracing callback out to a set of
240
+ tracers, so a request can be logged to a file and
241
+ exported to OpenTelemetry at the same time instead
242
+ of choosing between them. Its `on_request_start`
243
+ and `on_tool_start` return an array with one span
244
+ per tracer, and that array is passed back as
245
+ `span:` to the matching finish, error, or interrupt
246
+ callback, so each tracer is handed the span it
247
+ returned. A tracer that raises is reported rather
248
+ than taking the request down with it, and
249
+ `LLM::Interrupt` still travels up the stack.
250
+
251
+ * **tracer: add `LLM::Tracer.multi`** <br>
252
+ Add
253
+ [`LLM::Tracer.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method)
254
+ as a factory for `LLM::Tracer::Multi`. The
255
+ provider comes first and is required, so a
256
+ tracer given as a class is built with it,
257
+ while a tracer given as an instance is used
258
+ as it is. Builtin tracers and a custom one
259
+ can be mixed in a single call.
260
+
261
+ * **tracer: build every tracer given to `LLM::Tracer.multi`** <br>
262
+ Fix a bug where
263
+ [`LLM::Tracer.multi`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#multi-class_method)
264
+ passed its tracers to `LLM::Tracer::Multi.new` as
265
+ a single array rather than one argument each,
266
+ so a `Multi` held that array as its only
267
+ tracer and every callback raised `NoMethodError`
268
+ on it.
269
+ [`LLM::Tracer::Rescue`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Rescue.html)
270
+ reported the crash to standard error and
271
+ swallowed it, so the request was not traced.
272
+
273
+ * **tracer: alias `LLM::Tracer.pretty_logger` as `LLM::Tracer.pretty`** <br>
274
+ Add
275
+ [`LLM::Tracer.pretty`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#pretty_logger-class_method)
276
+ as an alias for `LLM::Tracer.pretty_logger`,
277
+ so the shorter name builds the same tracer
278
+ with the same options. The README uses
279
+ `pretty` throughout.
280
+
281
+ * **tracer: protect an agent from a tracer crash** <br>
282
+ The
283
+ [`LLM::Tracer::Rescue`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Rescue.html)
284
+ module prepends itself on every subclass of
285
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
286
+ and rescues uncaught exceptions that occur
287
+ in tracer callback. Any callback that lets
288
+ an exception exit its method will be rescued
289
+ and the exception dumped onto standard error.
290
+
291
+ * **tracer: run every `on_tool_*` callback on the agent's thread** <br>
292
+ The
293
+ [`LLM::Tracer#on_tool_start`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_start-instance_method),
294
+ `#on_tool_finish`, `#on_tool_error`, and
295
+ `#on_tool_interrupt` callbacks are now
296
+ called on the same thread as the agent
297
+ they belong to - and this is true
298
+ across all strategies. The previous
299
+ implementation could call the tracer
300
+ across threads and fibers when given
301
+ the `thread`, `async`, and `fiber`
302
+ strategies.
303
+
304
+ Both `ractor`, and `fork` already kept the
305
+ tracer on one thread, and now all strategies
306
+ do. The last exception to the rule was
307
+ `LLM::Tracer#on_interrupt`, which could be
308
+ called on the thread that asked for the
309
+ interrupt rather than the thread that
310
+ received the interrupt.
311
+
312
+ * **tracer: run `LLM::Tracer#on_interrupt` on the agent's thread** <br>
313
+ The
314
+ [`LLM::Tracer#on_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_interrupt-instance_method)
315
+ callback is now called on the agent's thread,
316
+ or fiber, the same as every other callback.
317
+ Before, it was called by whoever asked for the
318
+ interrupt, so a tracer could be called across
319
+ threads for an interrupt alone. The raise now
320
+ unwinds to the turn it stopped, the tracer is
321
+ told `scope: :agent` there, and the interrupt
322
+ is re-raised for the caller.
323
+
324
+ * **tracer: `on_interrupt` and `on_exit` raise `NotImplementedError`** <br>
325
+ The
326
+ [`LLM::Tracer#on_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_interrupt-instance_method)
327
+ and
328
+ [`LLM::Tracer#on_exit`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_exit-instance_method)
329
+ methods now raise `NotImplementedError`
330
+ by default and subclasses of
331
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
332
+ must implement them, otherwise
333
+ the tracer will crash and the crash
334
+ will be logged to standard error
335
+ (`$stderr`). A tracer crash does
336
+ not crash an agent, and both of
337
+ these methods can be implemented
338
+ as noops that return `nil`.
339
+
340
+ * **tracer: `on_tool_{finish,interrupt,error}` receive `started_at` / `finished_at`** <br>
341
+ The
342
+ [`LLM::Tracer#on_tool_finish`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_finish-instance_method),
343
+ [`LLM::Tracer#on_tool_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_interrupt-instance_method)
344
+ and
345
+ [`LLM::Tracer#on_tool_error`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_error-instance_method)
346
+ methods now receive `started_at`
347
+ and `finished_at` attributes that can
348
+ determine how long a tool took to run.
349
+ Both are based on a monotonic clock
350
+ (`Process::CLOCK_MONOTONIC`) and
351
+ neither are `Time` objects.
352
+
353
+ * **tracer: add `LLM::Tracer#on_tool_interrupt`** <br>
354
+ The [`LLM::Tracer#on_tool_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_interrupt-instance_method)
355
+ method allows a tracer to close a span that
356
+ was opened by
357
+ [`LLM::Tracer#on_tool_start`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_tool_start-instance_method)
358
+ and it is called for every tool that
359
+ was interrupted. The callback receives
360
+ the tool name and its tool call ID plus
361
+ the span that was opened by `on_tool_start`.
362
+
363
+ * **tracer: return a span from `LLM::Tracer::Logger#on_tool_start`** <br>
364
+ The
365
+ [`LLM::Tracer::Logger#on_tool_start`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html#on_tool_start-instance_method)
366
+ method now returns a span that can be
367
+ matched by another callback method that
368
+ will close the span (eg `on_tool_start -> on_tool_finish`).
369
+
370
+ * **tracer: return a span from `LLM::Tracer::PrettyLogger#on_tool_start`** <br>
371
+ The
372
+ [`LLM::Tracer::PrettyLogger#on_tool_start`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/PrettyLogger.html#on_tool_start-instance_method)
373
+ method now returns a span that can be
374
+ matched by another callback method that
375
+ will close the span (eg `on_tool_start -> on_tool_finish`).
376
+
377
+ * **tracer: add `LLM::Tracer::Logger#on_tool_interrupt`** <br>
378
+ This method implements the new callback available on the
379
+ superclass of Logger. The callback emits an `tool.interrupt`
380
+ event and carries other metadata with it.
381
+
382
+ * **tracer: add `LLM::Tracer::PrettyLogger#on_tool_interrupt`** <br>
383
+ This method implements the new callback available on the
384
+ superclass of PrettyLogger. The callback emits a log entry
385
+ that captures the tool interrupt via an IO object.
386
+
387
+ * **tracer: add `LLM::Tracer::Telemetry#on_tool_interrupt`** <br>
388
+ This method implements the new callback available on
389
+ the superclass of Telemetry. The callback records
390
+ `error.type` as `LLM::Interrupt`, marks the span as
391
+ an error, adds the standard `gen_ai.tool.finish`
392
+ event and closes the span that was opened by
393
+ `on_tool_start`. It follows the shape of
394
+ `on_tool_error` and `on_interrupt` instead of
395
+ emitting a `gen_ai.tool.interrupt` event, which is
396
+ not part of the OpenTelemetry standard.
397
+
398
+ * **tracer: `LLM::Tracer#on_interrupt` raises `NotImplementedError` by default** <br>
399
+ The
400
+ [`LLM::Tracer#on_interrupt`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_interrupt-instance_method)
401
+ method is no longer a noop that returns nil. All tracers
402
+ that subclass `LLM::Tracer` must implement this method. The
403
+ change makes `on_interrupt` consistent with all other
404
+ callback methods that are available on [LLM::Tracer](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html).
405
+
406
+ * **tracer: add `LLM::Tracer::Logger#on_interrupt`** <br>
407
+ This method implements the callback available on the
408
+ superclass of Logger. The callback emits an `info` event
409
+ that is named from the scope (eg `request.interrupt`)
410
+ and carries the scope and the request ID with it.
411
+
412
+ * **tracer: add `LLM::Tracer::PrettyLogger#on_interrupt`** <br>
413
+ This method implements the callback available on the
414
+ superclass of PrettyLogger. The callback writes a line
415
+ to its IO object that names the provider and the scope
416
+ the interrupt was received in.
417
+
418
+ * **tracer: add `LLM::Tracer::Telemetry#on_interrupt`** <br>
419
+ This method implements the callback available on the
420
+ superclass of Telemetry. A `:request` interrupt closes
421
+ the span that `on_request_start` opened and marks it
422
+ with an error and a `gen_ai.request.finish` event, since
423
+ a request is the only interrupted scope with a span left
424
+ open. The `:tool` and `:agent` scopes do nothing, because
425
+ tools are closed by `on_tool_interrupt`, and any other
426
+ scope raises `LLM::Error`.
427
+
428
+ * **tracer: `LLM::Tracer#on_exit` raises `NotImplementedError` by default** <br>
429
+ The
430
+ [`LLM::Tracer#on_exit`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html#on_exit-instance_method)
431
+ method is no longer a noop that returns nil. All tracers
432
+ that subclass `LLM::Tracer` must implement this method. The
433
+ change makes `on_exit` consistent with all other
434
+ callback methods that are available on [LLM::Tracer](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html).
435
+
436
+ ### LLM::Context
437
+
438
+ * **context: inherit database ID** <br>
439
+ The
440
+ [`LLM::Context#id`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#id-instance_method)
441
+ and
442
+ [`LLM::Agent#id`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#id-instance_method)
443
+ methods will inherit the database ID
444
+ of the record that is associated with
445
+ them __if__ a record is in scope and
446
+ __if__ the database ID is in the UUIDv7
447
+ format. Otherwise a random UUIDv7 is
448
+ generated as the ID. A record is normally
449
+ in scope when given an ActiveRecord or
450
+ Sequel model has wrapped an instance
451
+ of
452
+ [LLM::Context](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html)
453
+ /
454
+ [LLM::Agent](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html).
455
+
456
+ * **context: an ID must be in the UUIDv7 format** <br>
457
+ An agent and context ID must __always__ carry a
458
+ UUIDv7 format, even when given an explicit `id`
459
+ as a parameter, and anything else raises
460
+ `LLM::Error`. This is because the ID is used
461
+ internally to compute other attributes
462
+ (such as `LLM::Agent#created_at`). The runtime
463
+ checks it with
464
+ [`LLM::Utils.uuidv7?`](https://r.uby.dev/api-docs/llm.rb/LLM/Utils.html#uuidv7?-instance_method),
465
+ the same predicate `LLM::Utils.timestamp` reads
466
+ a time through, so an id this predicate accepts
467
+ is an id `created_at` can answer for.
468
+
469
+ ### Registry
470
+
471
+ * **refresh model metadata** <br>
472
+ Update `data/` with current listings, limits,
473
+ and pricing. Anthropic adds Claude Haiku 5.5,
474
+ Bedrock adds Claude Haiku 5.5, Claude Sonnet
475
+ 5.5 in the US and EU, Claude Sonnet 5 and
476
+ Claude Opus 5 in India, and GLM-5.3 in the US
477
+ and global regions, Mistral adds the Ministral
478
+ 3 3B, 8B, and 14B models, Mistral Large 4,
479
+ Codestral 25.08, Voxtral Small, GLM-5.2, and
480
+ Leanstral 1.5, DeepInfra adds
481
+ `Qwen/Qwen3-VL-30B-A3B-Instruct`, and
482
+ OpenRouter adds six models. OpenRouter drops
483
+ sixteen entries, twelve of them Qwen, and
484
+ Alibaba, Mistral, Moonshot, OpenAI, and xAI
485
+ correct limits and prices. Google adds Gemini 4
486
+ Argon, a reasoning model with a 1M-token context,
487
+ DeepInfra adds Claude Fable 5 and Mistral Small 3
488
+ and deprecates `Qwen/Qwen3.8-2.4T-A95B`, and
489
+ OpenRouter adds `liquid/lfm-2.5-24b-a2b`,
490
+ `google/gemini-4-argon`, the Scaledown extract
491
+ and summarize models, and the Pareto models,
492
+ drops `unbiased/pareto` and three Ling 3.0 Flash
493
+ and Apodex entries, and reprices several DeepSeek,
494
+ Z.ai, Tencent, Moonshot, Meta, and Gemma models.
495
+
496
+ ### Fix
497
+
498
+ * **tools: require `test-cmd.rb` `~> 2.7.3`** <br>
499
+ The exec-backed tools now need `test-cmd.rb`
500
+ 2.7.3 or later, up from 2.7.1. Version 2.7.2
501
+ fixes a race where `Process.wait` could claim a
502
+ process another thread had spawned, so a tool
503
+ could answer with a status and a pid that
504
+ belonged to another command, and 2.7.3 stops
505
+ looking for `/bin/false` when a command is not
506
+ found, which Windows does not have.
507
+
508
+ * **tools: name the tool that was not found** <br>
509
+ The in-band error returned when a model asks for
510
+ a tool that does not exist names it now, as
511
+ `tool 'exec' not found`, where it said only
512
+ `tool not found`. That is the same message
513
+ [`LLM::Tool.find_by_name!`](https://r.uby.dev/api-docs/llm.rb/LLM/Tool.html#find_by_name!-class_method)
514
+ raises when it cannot resolve a name, so the two
515
+ paths read alike: the class raises
516
+ [`LLM::NoSuchToolError`](https://r.uby.dev/api-docs/llm.rb/LLM/NoSuchToolError.html)
517
+ rather than the `no such tool "exec"` it used to.
518
+
519
+ ## v16.0.0
520
+
521
+ Changes since `v15.5.0`.
522
+
523
+ This release makes an interrupt precise: `LLM::Interrupt` moves outside
524
+ `StandardError`, a turn between its requests ends where it is, and a cancel is
525
+ delivered inside the tool that has to handle it. It also requires Ruby 3.4 or
526
+ later, reads a `:jsonb` record's messages from the column, saves a
527
+ record-backed conversation after each request, and adds
528
+ `LLM::ActiveRecord#messages!`, `LLM::Stream#on_step`, and a tracer's
529
+ `on_interrupt` hook.
530
+
9
531
  ### Breaking
10
532
 
11
533
  #### Migration