llm.rb 16.0.0 → 17.0.1

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