llm.rb 15.3.0 → 15.4.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +121 -3
  3. data/README.md +180 -64
  4. data/bin/llm.rb +9 -2
  5. data/data/alibaba.json +45 -0
  6. data/data/anthropic.json +67 -0
  7. data/data/bedrock.json +1466 -397
  8. data/data/deepinfra.json +148 -16
  9. data/data/deepseek.json +3 -0
  10. data/data/mistral.json +42 -0
  11. data/data/openai.json +186 -0
  12. data/data/openrouter.json +1538 -378
  13. data/data/xai.json +53 -20
  14. data/data/zai.json +90 -4
  15. data/docs/deepdive/advanced/compaction.md +1 -2
  16. data/docs/deepdive/advanced/context.md +214 -1
  17. data/docs/deepdive/advanced/guard.md +9 -57
  18. data/docs/deepdive/features/builtin_tools.md +14 -16
  19. data/docs/deepdive/features/console.md +5 -0
  20. data/docs/deepdive/features/database.md +85 -10
  21. data/docs/deepdive/fundamentals/agents.md +7 -8
  22. data/docs/deepdive/fundamentals/providers.md +45 -5
  23. data/docs/deepdive/fundamentals/schema.md +73 -0
  24. data/docs/deepdive/fundamentals/tools.md +80 -27
  25. data/docs/deepdive/media/audio.md +8 -19
  26. data/docs/deepdive/media/images.md +8 -10
  27. data/docs/deepdive/media/ocr.md +1 -3
  28. data/docs/deepdive/reference/cost.md +48 -0
  29. data/docs/deepdive/reference/tracer.md +76 -0
  30. data/docs/deepdive.md +1 -1
  31. data/lib/llm/active_record/message.rb +113 -0
  32. data/lib/llm/active_record.rb +1 -0
  33. data/lib/llm/agent.rb +28 -19
  34. data/lib/llm/console/buffer.rb +9 -1
  35. data/lib/llm/console.rb +6 -1
  36. data/lib/llm/context/deserializer.rb +6 -1
  37. data/lib/llm/context.rb +8 -4
  38. data/lib/llm/guard.rb +2 -8
  39. data/lib/llm/provider.rb +16 -0
  40. data/lib/llm/providers/alibaba.rb +15 -0
  41. data/lib/llm/providers/anthropic/error_handler.rb +5 -2
  42. data/lib/llm/providers/anthropic/files.rb +12 -12
  43. data/lib/llm/providers/anthropic/models.rb +2 -2
  44. data/lib/llm/providers/anthropic.rb +2 -2
  45. data/lib/llm/providers/bedrock/error_handler.rb +3 -2
  46. data/lib/llm/providers/bedrock/models.rb +5 -3
  47. data/lib/llm/providers/bedrock.rb +2 -2
  48. data/lib/llm/providers/deepinfra/audio.rb +4 -4
  49. data/lib/llm/providers/deepinfra/images.rb +4 -4
  50. data/lib/llm/providers/google/error_handler.rb +5 -2
  51. data/lib/llm/providers/google/files.rb +10 -10
  52. data/lib/llm/providers/google/images.rb +2 -2
  53. data/lib/llm/providers/google/models.rb +2 -2
  54. data/lib/llm/providers/google.rb +4 -4
  55. data/lib/llm/providers/ollama/error_handler.rb +5 -2
  56. data/lib/llm/providers/ollama/models.rb +2 -2
  57. data/lib/llm/providers/ollama.rb +4 -4
  58. data/lib/llm/providers/openai/audio.rb +6 -6
  59. data/lib/llm/providers/openai/error_handler.rb +5 -2
  60. data/lib/llm/providers/openai/files.rb +10 -10
  61. data/lib/llm/providers/openai/images.rb +4 -4
  62. data/lib/llm/providers/openai/models.rb +2 -2
  63. data/lib/llm/providers/openai/moderations.rb +2 -2
  64. data/lib/llm/providers/openai/responses.rb +6 -6
  65. data/lib/llm/providers/openai/vector_stores.rb +22 -22
  66. data/lib/llm/providers/openai.rb +4 -4
  67. data/lib/llm/providers/xai/images.rb +4 -4
  68. data/lib/llm/tracer/telemetry.rb +4 -4
  69. data/lib/llm/tracer.rb +11 -3
  70. data/lib/llm/transport/execution.rb +8 -4
  71. data/lib/llm/version.rb +1 -1
  72. data/llm.gemspec +2 -2
  73. metadata +5 -5
  74. data/lib/llm/guard/loop.rb +0 -89
@@ -134,6 +134,82 @@ OpenTelemetry.
134
134
  The tracer can also write to a file with the `path:` option to
135
135
  [`LLM::Tracer::Logger.new`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Logger.html#initialize-instance_method).
136
136
 
137
+ ### Hooks
138
+
139
+ #### Overview
140
+
141
+ [`LLM::Tracer`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer.html)
142
+ exposes one method per event in a request's lifecycle. A subclass
143
+ implements the events it cares about and routes them anywhere: a
144
+ logger, a metrics counter, or a database table.
145
+
146
+ #### How it works
147
+
148
+ Three hooks cover a provider request. `on_request_start` fires before
149
+ the request is sent and returns the span that `on_request_finish` and
150
+ `on_request_error` receive. Every request carries a `request_id`, a
151
+ UUIDv7 minted when the request begins and passed to all three hooks
152
+ for that request, so a tracer can correlate its events even when a
153
+ turn makes several requests.
154
+
155
+ Three more hooks cover a local tool call. `on_tool_start` fires before
156
+ the tool runs and returns the span that `on_tool_finish` and
157
+ `on_tool_error` receive.
158
+
159
+ A turn is additionally bracketed with `start_trace` and `stop_trace`.
160
+ The runtime calls them around every agent turn with a `trace_group_id`,
161
+ and a tracer that supports it (such as
162
+ [`LLM::Tracer::Telemetry`](https://r.uby.dev/api-docs/llm.rb/LLM/Tracer/Telemetry.html))
163
+ uses that id to give every span of the turn the same trace id:
164
+
165
+ ```ruby
166
+ class MyTracer < LLM::Tracer
167
+ def on_request_start(operation:, model: nil, **)
168
+ warn "start #{operation} #{model}"
169
+ end
170
+
171
+ def on_request_finish(operation:, res:, **)
172
+ warn "finish #{operation}"
173
+ end
174
+
175
+ def on_request_error(ex:, **)
176
+ warn "error #{ex.class}"
177
+ end
178
+
179
+ def on_tool_start(id:, name:, arguments:, model:, **)
180
+ warn "tool #{name}"
181
+ end
182
+
183
+ def on_tool_finish(result:, **)
184
+ warn "tool #{result.name} done"
185
+ end
186
+
187
+ def on_tool_error(ex:, **)
188
+ warn "tool error #{ex.class}"
189
+ end
190
+ end
191
+
192
+ llm = LLM.deepseek(key: ENV["KEY"])
193
+ llm.tracer = MyTracer.new(llm)
194
+ agent = LLM::Agent.new(llm)
195
+ agent.talk "Hello"
196
+ ```
197
+
198
+ #### Why would I use it?
199
+
200
+ The built-in tracers cover logging and OpenTelemetry. A hook lets you
201
+ send the same events somewhere else, and because the built-in tracers
202
+ accept the keywords they do not use, existing tracer code keeps working
203
+ as hooks gain parameters.
204
+
205
+ #### Notes
206
+
207
+ The base class raises `NotImplementedError` for any hook it does not
208
+ implement, so a tracer must cover every hook the runtime calls: the six
209
+ request and tool hooks above. Accept `**` to absorb keywords you do not
210
+ read, as the built-in tracers do, so a hook that gains a parameter does
211
+ not break your subclass.
212
+
137
213
  ### PrettyLogger
138
214
 
139
215
  #### Overview
data/docs/deepdive.md CHANGED
@@ -44,7 +44,7 @@ useful when you need to go beyond the basics.
44
44
  #### Notes
45
45
 
46
46
  The deepdive is a living document. Sections are added as new
47
- features land. The [README.md](https://github.com/r-uby-dev/llm#readme)
47
+ features land. The [README.md](https://github.com/r-uby-dev/llm.rb#readme)
48
48
  is the best place to start if you are new to llm.rb.
49
49
 
50
50
  ---
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM::ActiveRecord
4
+ ##
5
+ # Represents a message in an agent's memory.
6
+ #
7
+ # This class is virtual and never materializes
8
+ # in the database as a real table. It provides
9
+ # a SQL view into the messages array stored in
10
+ # the JSONB column that carries an agent's
11
+ # runtime state, and it returns relations, so
12
+ # messages can be filtered and ordered in the
13
+ # database instead of in memory.
14
+ #
15
+ # It expects the agent and this class to share a
16
+ # connection, which they do when both live on the
17
+ # same database - the default in Rails.
18
+ #
19
+ # @example
20
+ # LLM::ActiveRecord::Message.for(agent:)
21
+ # .where(role: "assistant")
22
+ # .count
23
+ class Message < ActiveRecord::Base
24
+ ##
25
+ # The name the derived table is given. It is "created"
26
+ # on-demand and it is here so that ActiveRecord has
27
+ # something to select from.
28
+ self.table_name = "llm_agent_messages"
29
+
30
+ ##
31
+ # The derived table does not exist, so there is no schema
32
+ # to load. Without this, ActiveRecord asks the database
33
+ # for the columns of a table that was never created.
34
+ # @return [void]
35
+ def self.load_schema!
36
+ @columns_hash = {}.freeze
37
+ end
38
+
39
+ ##
40
+ # Build a relation over one agent's messages.
41
+ #
42
+ # The table, the column and its type come from the
43
+ # agent's own class, so this works for any model that
44
+ # keeps its conversation in a column.
45
+ #
46
+ # The base fields the query guarantees are columns -
47
+ # id, role, content, tools - so they are what a
48
+ # caller's `where` and `order` are written against.
49
+ # The whole message is carried along as `data`, for
50
+ # fields the query does not name yet.
51
+ #
52
+ # The derived table is aliased as `llm_agent_messages`
53
+ # because ActiveRecord qualifies its SELECT with the
54
+ # class' table name.
55
+ #
56
+ # @param [ActiveRecord::Base] agent
57
+ # An instance of an ActiveRecord model.
58
+ # @return [ActiveRecord::Relation]
59
+ def self.for(agent:)
60
+ klass = agent.class
61
+ connection = klass.connection
62
+ table = connection.quote_table_name(klass.table_name)
63
+ options = klass.llm_plugin_options
64
+ column = connection.quote_column_name(options.fetch(:data_column))
65
+ from(<<~SQL).where(agent_id: agent&.id)
66
+ (SELECT #{table}.id AS agent_id,
67
+ (message ->> 'id') AS id,
68
+ (message ->> 'role') AS role,
69
+ (message ->> 'content') AS content,
70
+ (message -> 'tools') AS tools,
71
+ ordinality AS position,
72
+ message AS data
73
+ FROM #{table},
74
+ jsonb_array_elements(#{table}.#{column} -> 'messages')
75
+ WITH ORDINALITY AS each(message, ordinality))
76
+ AS llm_agent_messages
77
+ SQL
78
+ end
79
+
80
+ ##
81
+ # @return (see LLM::Message#tool_call?)
82
+ def tool_call?
83
+ unwrap!.tool_call?
84
+ end
85
+
86
+ ##
87
+ # @return (see LLM::Message#tool_return?)
88
+ def tool_return?
89
+ unwrap!.tool_return?
90
+ end
91
+
92
+ ##
93
+ # The message, as the runtime would hand it back.
94
+ #
95
+ # The whole serialized message is used, so fields the
96
+ # query does not name: usage, reasoning content,
97
+ # and compaction survive the round trip through the
98
+ # database.
99
+ #
100
+ # @return [LLM::Message]
101
+ def unwrap!
102
+ @message ||= begin
103
+ stored = data.is_a?(Hash) ? data : {}
104
+ extra = stored.each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value }
105
+ extra[:id] = stored["id"] || id
106
+ extra[:role] ||= role
107
+ extra[:content] ||= content
108
+ extra[:tool_calls] = stored["tools"] || tools
109
+ LLM::Message.new(extra[:role], extra[:content], extra)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -74,4 +74,5 @@ module LLM::ActiveRecord
74
74
 
75
75
  require "llm/active_record/acts_as_llm"
76
76
  require "llm/active_record/acts_as_agent"
77
+ require "llm/active_record/message"
77
78
  end
data/lib/llm/agent.rb CHANGED
@@ -17,10 +17,6 @@ module LLM
17
17
  # **Notes:**
18
18
  # * Instructions are injected once unless a system message is already present.
19
19
  # * An agent automatically executes tool loops (unlike {LLM::Context LLM::Context}).
20
- # * The automatic tool loop enables the wrapped context's `guard` by default.
21
- # The built-in {LLM::Guard::Loop LLM::Guard::Loop} detects repeated
22
- # tool-call patterns and blocks stuck execution before more tool work is
23
- # queued.
24
20
  # * The tool loop can be bounded with `tool_budget`. Once the budget is
25
21
  # spent, no further tool calls are run for that turn: the agent sends an
26
22
  # in-band advisory message back through the model instead, and keeps
@@ -106,6 +102,8 @@ module LLM
106
102
  # end
107
103
  #
108
104
  # @param [Hash] properties
105
+ # @option properties [String, Symbol, Proc] :name
106
+ # @option properties [String, Symbol, Proc] :description
109
107
  # @option properties [String] :instructions
110
108
  # @option properties [String] :model
111
109
  # @option properties [Array<LLM::Function>] :tools
@@ -138,7 +136,7 @@ module LLM
138
136
  # @return [String]
139
137
  # Return's the agents name
140
138
  def self.name(name = UNDEFINED, &block)
141
- if name.equal?(UNDEFINED)
139
+ if name.equal?(UNDEFINED) and block.nil?
142
140
  if @name.nil?
143
141
  name = to_s.split("::").last
144
142
  @name = name.gsub(CASE_PATTERN, "-").downcase
@@ -158,6 +156,11 @@ module LLM
158
156
  ##
159
157
  # Set or get an agent's description
160
158
  # @note
159
+ # Reading this on the class returns what was configured - a
160
+ # Symbol or Proc included - because an instance is what
161
+ # resolves those. Read it on an instance for the description
162
+ # itself.
163
+ # @note
161
164
  # This method serves as a self-documenting string.
162
165
  # It is optional but recommended.
163
166
  # @param [String] desc
@@ -165,7 +168,7 @@ module LLM
165
168
  # @return [String, nil]
166
169
  # Returns the agent's description
167
170
  def self.description(desc = UNDEFINED, &block)
168
- if desc.equal?(UNDEFINED)
171
+ if desc.equal?(UNDEFINED) and block.nil?
169
172
  @desc
170
173
  else
171
174
  @desc = block || desc
@@ -338,10 +341,10 @@ module LLM
338
341
  # The path to a file
339
342
  # @return [String, nil]
340
343
  def self.path(path = UNDEFINED, &block)
341
- if path.equal?(UNDEFINED)
344
+ if path.equal?(UNDEFINED) and block.nil?
342
345
  @path
343
346
  else
344
- @path = path || block
347
+ @path = block || path
345
348
  end
346
349
  end
347
350
 
@@ -365,10 +368,10 @@ module LLM
365
368
  # a single turn.
366
369
  # @return [Integer, nil]
367
370
  def self.tool_budget(budget = UNDEFINED, &block)
368
- if budget.equal?(UNDEFINED)
371
+ if budget.equal?(UNDEFINED) and block.nil?
369
372
  @tool_budget
370
373
  else
371
- @tool_budget = budget || block
374
+ @tool_budget = block || budget
372
375
  end
373
376
  end
374
377
 
@@ -434,13 +437,11 @@ module LLM
434
437
  end
435
438
  end
436
439
  ##
437
- # Alibaba (token plan) will frequently issue rate
438
- # limits or time outs that it recovers from. The
439
- # higher retry count is to account for scenarios
440
- # where it takes longer than expected to recover.
441
- retry_budget = llm.name == :alibaba ? 8 : 5
442
- params[:retry_budget] = retry_budget if params[:retry_budget].equal?(UNDEFINED)
443
- @ctx = LLM::Context.new(llm, {guard: LLM::Guard::Loop}.merge(params))
440
+ # The provider decides how patient a turn should be: it
441
+ # knows its own API, and how often it recovers from a
442
+ # rate limit rather than failing outright.
443
+ params[:retry_budget] = llm.retry_budget if params[:retry_budget].equal?(UNDEFINED)
444
+ @ctx = LLM::Context.new(llm, params)
444
445
  @path and File.readable?(@path) ? @ctx.restore(path:) : nil
445
446
  end
446
447
 
@@ -867,8 +868,16 @@ module LLM
867
868
  end
868
869
  res
869
870
  end
870
- return run.call unless @tracer
871
- @llm.with_tracer(@tracer, &run)
871
+ ##
872
+ # One turn, one trace group. A tracer
873
+ # is told where the turn begins and
874
+ # where it ends. The trace group ID
875
+ # identifies the turn.
876
+ tracer = @tracer || @llm.tracer
877
+ tracer.start_trace(name: "llm.turn", trace_group_id: SecureRandom.uuid_v7)
878
+ @llm.with_tracer(tracer, &run)
879
+ ensure
880
+ tracer&.stop_trace
872
881
  end
873
882
 
874
883
  ##
@@ -72,6 +72,14 @@ class LLM::Console
72
72
  @snapshot = nil
73
73
  end
74
74
 
75
+ ##
76
+ # Returns true while the buffer has an active row
77
+ # for the streaming path to replace.
78
+ # @return [Boolean]
79
+ def open?
80
+ !@snapshot.nil?
81
+ end
82
+
75
83
  ##
76
84
  # @return [void]
77
85
  def scroll_up(height)
@@ -131,7 +139,7 @@ class LLM::Console
131
139
  # One or more chunks.
132
140
  # @return [void]
133
141
  def replace(chunks)
134
- @rows = @snapshot.map(&:dup)
142
+ @rows = @snapshot ? @snapshot.map(&:dup) : @rows
135
143
  chunks.each { wrap(_1, @rows) }
136
144
  end
137
145
 
data/lib/llm/console.rb CHANGED
@@ -284,6 +284,11 @@ module LLM
284
284
  # UI can repaint. Cap the stream chunks drained per call so
285
285
  # read! gives control back to the key loop: leftover chunks
286
286
  # stay queued and are drained by the next read!.
287
+ #
288
+ # A turn that was cancelled can still have chunks queued, and
289
+ # they arrive after the buffer is closed. There is no active
290
+ # row to replace at that point, so they are dropped.
291
+ next unless buffer.open?
287
292
  status.text = think_text if stream.tools.empty?
288
293
  write_message name, markdown(value), method: :replace
289
294
  burst += 1
@@ -292,7 +297,7 @@ module LLM
292
297
  self.status = value
293
298
  when :done
294
299
  status.text = "idle"
295
- write_message name, markdown(value), method: :replace
300
+ write_message(name, markdown(value), method: :replace) if buffer.open?
296
301
  buffer.close
297
302
  @thread = nil
298
303
  when :cancel
@@ -5,7 +5,12 @@ class LLM::Context
5
5
  # @api private
6
6
  module Deserializer
7
7
  ##
8
- # Restore a saved context state
8
+ # Restore a saved context state.
9
+ #
10
+ # The `context_used` and `context_window` keys carried by a payload
11
+ # are projections for queryability. They are deliberately ignored
12
+ # here: the runtime derives both from the messages and the
13
+ # registry, so a payload can never seed them.
9
14
  # @param [String, nil] path
10
15
  # The path to a JSON file
11
16
  # @param [String, nil] string
data/lib/llm/context.rb CHANGED
@@ -183,10 +183,6 @@ module LLM
183
183
  # via {LLM::Stream#on_tool_call}. A blocked call yields its in-band
184
184
  # `guard_error` return without executing.
185
185
  #
186
- # The built-in implementation is {LLM::Guard::Loop LLM::Guard::Loop}, which
187
- # detects repeated tool-call patterns and turns them into in-band
188
- # `guard_error` tool returns.
189
- #
190
186
  # @return [Class<LLM::Guard>]
191
187
  def guard
192
188
  @guard[:klass]
@@ -506,6 +502,12 @@ module LLM
506
502
  end
507
503
 
508
504
  ##
505
+ # Returns the runtime state as a Hash.
506
+ #
507
+ # `context_used` and `context_window` are projections written for
508
+ # queryability, so that saved state can be inspected at rest
509
+ # without loading it. They are never read back; the runtime
510
+ # always derives them from the messages and the registry.
509
511
  # @return [Hash]
510
512
  def to_h
511
513
  {
@@ -513,6 +515,8 @@ module LLM
513
515
  id: @id,
514
516
  model:,
515
517
  compacted:,
518
+ context_used:,
519
+ context_window:,
516
520
  messages: @messages.map { serialize_message(_1) }
517
521
  }
518
522
  end
data/lib/llm/guard.rb CHANGED
@@ -10,16 +10,10 @@ module LLM
10
10
  # an {LLM::Function::Return LLM::Function::Return} that closes the
11
11
  # pending tool call, or `nil` when execution should continue. The guard
12
12
  # is stamped onto the functions the context binds, so it runs whenever a
13
- # task is spawned — including tool calls queued from a stream. The
14
- # built-in implementation is {LLM::Guard::Loop LLM::Guard::Loop}, which
15
- # detects repeated tool-call patterns. {LLM::Guard::Null LLM::Guard::Null}
16
- # is a no-op and the default.
17
- #
18
- # {LLM::Agent LLM::Agent} enables {LLM::Guard::Loop LLM::Guard::Loop}
19
- # by default through its wrapped context.
13
+ # task is spawned — including tool calls queued from a stream.
14
+ # {LLM::Guard::Null LLM::Guard::Null} is a no-op and the default.
20
15
  class Guard
21
16
  require_relative "guard/null"
22
- require_relative "guard/loop"
23
17
 
24
18
  ##
25
19
  # @return [LLM::Context]
data/lib/llm/provider.rb CHANGED
@@ -255,6 +255,22 @@ class LLM::Provider
255
255
  raise NotImplementedError
256
256
  end
257
257
 
258
+ ##
259
+ # Returns the number of times a rate-limited
260
+ # request is retried before the error is raised.
261
+ #
262
+ # Each retry sleeps a growing interval, so a budget
263
+ # that is exhausted surfaces the rate-limit error
264
+ # instead of blocking indefinitely. A provider can
265
+ # return a different budget when its API recovers
266
+ # from rate limits more slowly.
267
+ #
268
+ # @see LLM::Agent#retry_budget
269
+ # @return [Integer]
270
+ def retry_budget
271
+ 5
272
+ end
273
+
258
274
  ##
259
275
  # Returns an object that can generate a JSON schema
260
276
  # @return [LLM::Schema]
@@ -89,5 +89,20 @@ module LLM
89
89
  def default_model
90
90
  "deepseek-v4-flash-0731"
91
91
  end
92
+
93
+ ##
94
+ # Returns the number of times a rate-limited
95
+ # request is retried.
96
+ #
97
+ # Alibaba (token plan) will frequently issue
98
+ # rate limits or time outs that it recovers from.
99
+ # The higher retry count is to account for scenarios
100
+ # where it takes longer than expected to recover.
101
+ #
102
+ # @see LLM::Provider#retry_budget
103
+ # @return [Integer]
104
+ def retry_budget
105
+ 8
106
+ end
92
107
  end
93
108
  end
@@ -21,10 +21,13 @@ class LLM::Anthropic
21
21
  # The span
22
22
  # @param [LLM::Transport::Response, Net::HTTPResponse] res
23
23
  # The response from the server
24
+ # @param [String] request_id
25
+ # The id of the request that failed
24
26
  # @return [LLM::Anthropic::ErrorHandler]
25
- def initialize(tracer, span, res)
27
+ def initialize(tracer, span, res, request_id)
26
28
  @tracer = tracer
27
29
  @span = span
30
+ @request_id = request_id
28
31
  @res = LLM::Transport::Response.from(res)
29
32
  end
30
33
 
@@ -33,7 +36,7 @@ class LLM::Anthropic
33
36
  # Raises a subclass of {LLM::Error LLM::Error}
34
37
  def raise_error!
35
38
  ex = error
36
- @tracer.on_request_error(ex:, span:)
39
+ @tracer.on_request_error(ex:, span:, request_id: @request_id)
37
40
  ensure
38
41
  raise(ex) if ex
39
42
  end
@@ -38,9 +38,9 @@ class LLM::Anthropic
38
38
  def all(**params)
39
39
  query = URI.encode_www_form(params)
40
40
  req = LLM::Transport::Request.get("/v1/files?#{query}", headers)
41
- res, span, tracer = execute(request: req, operation: "request")
41
+ res, span, tracer, request_id = execute(request: req, operation: "request")
42
42
  res = ResponseAdapter.adapt(res, type: :enumerable)
43
- tracer.on_request_finish(operation: "request", res:, span:)
43
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
44
44
  res
45
45
  end
46
46
 
@@ -59,9 +59,9 @@ class LLM::Anthropic
59
59
  req = LLM::Transport::Request.post("/v1/files", headers)
60
60
  req["content-type"] = multi.content_type
61
61
  transport.set_body_stream(req, multi.body)
62
- res, span, tracer = execute(request: req, operation: "request")
62
+ res, span, tracer, request_id = execute(request: req, operation: "request")
63
63
  res = ResponseAdapter.adapt(res, type: :file)
64
- tracer.on_request_finish(operation: "request", res:, span:)
64
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
65
65
  res
66
66
  end
67
67
 
@@ -80,9 +80,9 @@ class LLM::Anthropic
80
80
  file_id = file.respond_to?(:id) ? file.id : file
81
81
  query = URI.encode_www_form(params)
82
82
  req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
83
- res, span, tracer = execute(request: req, operation: "request")
83
+ res, span, tracer, request_id = execute(request: req, operation: "request")
84
84
  res = ResponseAdapter.adapt(res, type: :file)
85
- tracer.on_request_finish(operation: "request", res:, span:)
85
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
86
86
  res
87
87
  end
88
88
 
@@ -101,9 +101,9 @@ class LLM::Anthropic
101
101
  query = URI.encode_www_form(params)
102
102
  file_id = file.respond_to?(:id) ? file.id : file
103
103
  req = LLM::Transport::Request.get("/v1/files/#{file_id}?#{query}", headers)
104
- res, span, tracer = execute(request: req, operation: "request")
104
+ res, span, tracer, request_id = execute(request: req, operation: "request")
105
105
  res = ResponseAdapter.adapt(res, type: :file)
106
- tracer.on_request_finish(operation: "request", res:, span:)
106
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
107
107
  res
108
108
  end
109
109
  alias_method :retrieve_metadata, :get_metadata
@@ -121,9 +121,9 @@ class LLM::Anthropic
121
121
  def delete(file:)
122
122
  file_id = file.respond_to?(:id) ? file.id : file
123
123
  req = LLM::Transport::Request.delete("/v1/files/#{file_id}", headers)
124
- res, span, tracer = execute(request: req, operation: "request")
124
+ res, span, tracer, request_id = execute(request: req, operation: "request")
125
125
  res = LLM::Response.new(res)
126
- tracer.on_request_finish(operation: "request", res:, span:)
126
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
127
127
  res
128
128
  end
129
129
 
@@ -147,9 +147,9 @@ class LLM::Anthropic
147
147
  file_id = file.respond_to?(:id) ? file.id : file
148
148
  req = LLM::Transport::Request.get("/v1/files/#{file_id}/content?#{query}", headers)
149
149
  io = StringIO.new("".b)
150
- res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
150
+ res, span, tracer, request_id = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
151
151
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
152
- tracer.on_request_finish(operation: "request", res:, span:)
152
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
153
153
  res
154
154
  end
155
155
 
@@ -40,9 +40,9 @@ class LLM::Anthropic
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
42
  req = LLM::Transport::Request.get("/v1/models?#{query}", headers)
43
- res, span, tracer = execute(request: req, operation: "request")
43
+ res, span, tracer, request_id = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :models)
45
- tracer.on_request_finish(operation: "request", res:, span:)
45
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
46
46
  res
47
47
  end
48
48
 
@@ -52,10 +52,10 @@ module LLM
52
52
  def complete(prompt, params = {})
53
53
  params, stream, tools, role = normalize_complete_params(params)
54
54
  req = build_complete_request(prompt, params, role)
55
- res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
55
+ res, span, tracer, request_id = execute(request: req, stream: stream, operation: "chat", model: params[:model])
56
56
  res = ResponseAdapter.adapt(res, type: :completion)
57
57
  .extend(Module.new { define_method(:__tools__) { tools } })
58
- tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
58
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
59
59
  res
60
60
  end
61
61
 
@@ -23,9 +23,10 @@ class LLM::Bedrock
23
23
  # @param [Object, nil] span
24
24
  # @param [LLM::Transport::Response, Net::HTTPResponse] res
25
25
  # @return [LLM::Bedrock::ErrorHandler]
26
- def initialize(tracer, span, res)
26
+ def initialize(tracer, span, res, request_id)
27
27
  @tracer = tracer
28
28
  @span = span
29
+ @request_id = request_id
29
30
  @res = LLM::Transport::Response.from(res)
30
31
  end
31
32
 
@@ -33,7 +34,7 @@ class LLM::Bedrock
33
34
  # @raise [LLM::Error]
34
35
  def raise_error!
35
36
  ex = error
36
- @tracer.on_request_error(ex:, span:)
37
+ @tracer.on_request_error(ex:, span:, request_id: @request_id)
37
38
  ensure
38
39
  raise(ex) if ex
39
40
  end
@@ -40,9 +40,10 @@ class LLM::Bedrock
40
40
  # @return [LLM::Response]
41
41
  def all(**params)
42
42
  host = credentials.host
43
+ request_id = SecureRandom.uuid_v7
43
44
  req = build_request(host, params)
44
45
  res = build_transport(host).request(req, owner: self)
45
- handle_response(res)
46
+ handle_response(res, request_id)
46
47
  end
47
48
 
48
49
  private
@@ -69,9 +70,10 @@ class LLM::Bedrock
69
70
 
70
71
  ##
71
72
  # @param [LLM::Transport::Response, Net::HTTPResponse] res
73
+ # @param [String] request_id
72
74
  # @return [LLM::Response]
73
75
  # @raise [LLM::Error]
74
- def handle_response(res)
76
+ def handle_response(res, request_id)
75
77
  res = LLM::Transport::Response.from(res)
76
78
  if res.success?
77
79
  res.body = LLM::Object.from(LLM.json.load(res.body || "{}"))
@@ -79,7 +81,7 @@ class LLM::Bedrock
79
81
  else
80
82
  body = +""
81
83
  res.read_body { body << _1 } if res.body.nil?
82
- LLM::Bedrock::ErrorHandler.new(tracer, nil, res).raise_error!
84
+ LLM::Bedrock::ErrorHandler.new(tracer, nil, res, request_id).raise_error!
83
85
  end
84
86
  end
85
87
 
@@ -89,10 +89,10 @@ module LLM
89
89
  tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
90
90
  sign!(req, body)
91
91
  model_id = model_id_for(req.path)
92
- res, span, tracer = execute(request: req, stream:, operation: "chat", stream_parser:, model: model_id)
92
+ res, span, tracer, request_id = execute(request: req, stream:, operation: "chat", stream_parser:, model: model_id)
93
93
  res = ResponseAdapter.adapt(res, type: :completion)
94
94
  .extend(Module.new { define_method(:__tools__) { tools } })
95
- tracer.on_request_finish(operation: "chat", model: model_id, res:, span:)
95
+ tracer.on_request_finish(operation: "chat", model: model_id, res:, span:, request_id:)
96
96
  res
97
97
  end
98
98