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
data/lib/llm/provider.rb CHANGED
@@ -32,7 +32,7 @@ class LLM::Provider
32
32
  # Requires the net-http-persistent gem.
33
33
  # @param [LLM::Transport, Class, nil] transport
34
34
  # Optional override with any {LLM::Transport} instance or subclass.
35
- def initialize(key:, host:, port: 443, timeout: 600, read_timeout: nil, connect_timeout: 5, ssl: true, base_path: "", persistent: false, transport: nil)
35
+ def initialize(key:, host:, port: 443, timeout: 240, read_timeout: nil, connect_timeout: 5, ssl: true, base_path: "", persistent: false, transport: nil)
36
36
  @key = key
37
37
  @host = host
38
38
  @port = port
@@ -482,6 +482,21 @@ class LLM::Provider
482
482
  raise NotImplementedError
483
483
  end
484
484
 
485
+ ##
486
+ # Returns an array of exceptions that a request
487
+ # could raise and that will be retried up to
488
+ # {LLM::Provider#retry_budget} times.
489
+ # @return [Array<Exception>]
490
+ def retry_errors
491
+ [
492
+ LLM::InsufficientQuotaError,
493
+ LLM::RateLimitError,
494
+ Net::ReadTimeout,
495
+ Net::WriteTimeout,
496
+ Net::OpenTimeout
497
+ ]
498
+ end
499
+
485
500
  private
486
501
 
487
502
  def path(suffix, base_path: true)
@@ -102,7 +102,7 @@ class LLM::Anthropic
102
102
  fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
103
103
  fn.tracer = @stream.extra[:tracer]
104
104
  fn.model = @stream.extra[:model]
105
- fn.guard = @stream.extra[:guard]
105
+ fn.intercept = @stream.extra[:intercept]
106
106
  end
107
107
  [fn, (registered ? nil : fn.unavailable)]
108
108
  end
@@ -181,7 +181,7 @@ class LLM::Bedrock
181
181
  f.arguments = payload["input"] || {}
182
182
  f.tracer = @stream.extra[:tracer]
183
183
  f.model = @stream.extra[:model]
184
- f.guard = @stream.extra[:guard]
184
+ f.intercept = @stream.extra[:intercept]
185
185
  end
186
186
  [fn, registered ? nil : fn.unavailable]
187
187
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::DeepSeek
4
+ ##
5
+ # The {LLM::DeepSeek::Files LLM::DeepSeek::Files} class provides a files
6
+ # object for interacting with [DeepSeek's Files API](https://api-docs.deepseek.com/guides/files_api).
7
+ # The files API lets a client upload images and reference them later by
8
+ # `file_id` - for example in chat requests to `deepseek-flash` - without
9
+ # re-uploading them. DeepSeek supports the JPEG, PNG, GIF, and WebP formats,
10
+ # and a single file may be at most 64 MiB.
11
+ #
12
+ # DeepSeek's Files API is OpenAI-compatible, with two differences from
13
+ # OpenAI's: it is served from the root of the API host (not under `/v1`),
14
+ # and the `purpose` of an upload must be `"user_data"`.
15
+ #
16
+ # @example example #1
17
+ # #!/usr/bin/env ruby
18
+ # require "llm"
19
+ #
20
+ # llm = LLM.deepseek(key: ENV["KEY"])
21
+ # file = llm.files.create file: "/images/photo.jpg"
22
+ # print "id: ", file.id, "\n"
23
+ class Files < LLM::OpenAI::Files
24
+ ##
25
+ # Create a file
26
+ # @example
27
+ # llm = LLM.deepseek(key: ENV["KEY"])
28
+ # res = llm.files.create file: "/images/photo.jpg"
29
+ # @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
30
+ # @param [File, LLM::File, String] file The file
31
+ # @param [String] purpose The purpose of the file (DeepSeek only supports "user_data")
32
+ # @param [Hash] params Other parameters (see DeepSeek docs)
33
+ # @raise (see LLM::Provider#request)
34
+ # @return [LLM::Response]
35
+ def create(file:, purpose: "user_data", **params)
36
+ super
37
+ end
38
+
39
+ private
40
+
41
+ ##
42
+ # DeepSeek serves its Files API from the root of the API host,
43
+ # while the rest of its OpenAI-compatible API lives under `/v1`
44
+ # @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
45
+ def path(suffix, **)
46
+ @provider.send(:path, suffix, base_path: false)
47
+ end
48
+ end
49
+ end
@@ -35,7 +35,7 @@ module LLM::DeepSeek::RequestAdapter
35
35
  when String
36
36
  [{type: :text, text: content.to_s}]
37
37
  when LLM::Response
38
- no_files_api!("response")
38
+ adapt_remote_file(content)
39
39
  when LLM::Message
40
40
  adapt_content(content.content)
41
41
  when LLM::Function::Return
@@ -57,7 +57,7 @@ module LLM::DeepSeek::RequestAdapter
57
57
  when :local_file
58
58
  adapt_local_file(object.value)
59
59
  when :remote_file
60
- no_files_api!("remote file")
60
+ adapt_remote_file(object.value)
61
61
  else
62
62
  prompt_error!(object)
63
63
  end
@@ -80,17 +80,19 @@ module LLM::DeepSeek::RequestAdapter
80
80
  end
81
81
 
82
82
  ##
83
- # Raises for a file that lives on the provider's side. DeepSeek
84
- # has no Files API, so a file id that another provider minted
85
- # cannot be resolved here.
86
- # @param [String] subject
87
- # A description of the rejected content
88
- # @raise [LLM::PromptError]
89
- # @return [void]
90
- def no_files_api!(subject)
91
- raise LLM::PromptError, "The given #{subject} is not supported by the DeepSeek " \
92
- "chat completions API: DeepSeek has no Files API, so a " \
93
- "file id cannot be resolved"
83
+ # Adapts a file that lives on the provider's side as a `file` content
84
+ # block that references it by `file_id`, which DeepSeek's chat
85
+ # completions API accepts for images uploaded through its Files API
86
+ # @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
87
+ # @param [LLM::Object, LLM::Response] file The file
88
+ # @return [Array<Hash>]
89
+ # @raise [LLM::PromptError] when the given object is not a file
90
+ def adapt_remote_file(file)
91
+ if file.file?
92
+ [{type: :file, file_id: file.id}]
93
+ else
94
+ prompt_error!(file)
95
+ end
94
96
  end
95
97
 
96
98
  def adapt_message
@@ -20,6 +20,7 @@ module LLM
20
20
  class DeepSeek < OpenAI
21
21
  require_relative "deepseek/request_adapter"
22
22
  require_relative "deepseek/response_adapter"
23
+ require_relative "deepseek/files"
23
24
  require_relative "deepseek/images"
24
25
  include DeepSeek::RequestAdapter
25
26
 
@@ -38,9 +39,11 @@ module LLM
38
39
  end
39
40
 
40
41
  ##
41
- # @raise [NotImplementedError]
42
+ # Provides an interface to DeepSeek's files API
43
+ # @see https://api-docs.deepseek.com/guides/files_api DeepSeek docs
44
+ # @return [LLM::DeepSeek::Files]
42
45
  def files
43
- raise NotImplementedError
46
+ LLM::DeepSeek::Files.new(self)
44
47
  end
45
48
 
46
49
  ##
@@ -80,5 +83,16 @@ module LLM
80
83
  def default_model
81
84
  "deepseek-flash"
82
85
  end
86
+
87
+ ##
88
+ # @note
89
+ # This method extends the default errors
90
+ # that are retried to include LLM::ServerError -
91
+ # a common error that DeepSeek returns when
92
+ # it is overloaded.
93
+ # @return [Array<Exception>]
94
+ def retry_errors
95
+ super.concat [LLM::ServerError]
96
+ end
83
97
  end
84
98
  end
@@ -150,7 +150,7 @@ class LLM::Google
150
150
  fn.arguments = call["args"]
151
151
  fn.tracer = @stream.extra[:tracer]
152
152
  fn.model = @stream.extra[:model]
153
- fn.guard = @stream.extra[:guard]
153
+ fn.intercept = @stream.extra[:intercept]
154
154
  end
155
155
  [fn, (registered ? nil : fn.unavailable)]
156
156
  end
@@ -265,7 +265,7 @@ class LLM::OpenAI
265
265
  fn.arguments = arguments
266
266
  fn.tracer = @stream.extra[:tracer]
267
267
  fn.model = @stream.extra[:model]
268
- fn.guard = @stream.extra[:guard]
268
+ fn.intercept = @stream.extra[:intercept]
269
269
  end
270
270
  [fn, (registered ? nil : fn.unavailable)]
271
271
  end
@@ -180,7 +180,7 @@ class LLM::OpenAI
180
180
  fn.arguments = arguments
181
181
  fn.tracer = @stream.extra[:tracer]
182
182
  fn.model = @stream.extra[:model]
183
- fn.guard = @stream.extra[:guard]
183
+ fn.intercept = @stream.extra[:intercept]
184
184
  end
185
185
  [fn, (registered ? nil : fn.unavailable)]
186
186
  end
data/lib/llm/stream.rb CHANGED
@@ -132,14 +132,15 @@ module LLM
132
132
  end
133
133
 
134
134
  ##
135
- # Called when a streamed tool call has been fully constructed.
136
- # A stream implementation may start tool execution here by pushing
137
- # `@queue << tool.task(:thread).tap(&:spawn)` onto {#queue} — `spawn`
138
- # returns nil, so `tap` keeps the task while starting it. The task
139
- # carries the context's guard, so it is checked before the tool runs —
140
- # a blocked call yields its guard return without executing.
135
+ # This method is called when the model
136
+ # requests a tool call. A little known
137
+ # feature: it is possible to spawn a tool
138
+ # call during a stream through:
139
+ #
140
+ # @queue << tool.task(:thread).tap(&:spawn)
141
+ #
141
142
  # @param [LLM::Function] tool
142
- # The parsed tool call.
143
+ # An instance of {LLM::Function LLM::Function}
143
144
  # @return [nil]
144
145
  def on_tool_call(tool)
145
146
  nil
data/lib/llm/tool.rb CHANGED
@@ -205,7 +205,7 @@ class LLM::Tool
205
205
  # @return [Class<LLM::Tool>]
206
206
  # @raise [LLM::NoSuchToolError]
207
207
  def self.find_by_name!(name)
208
- find_by_name(name) || raise(LLM::NoSuchToolError, "no such tool #{name.inspect}")
208
+ find_by_name(name) || raise(LLM::NoSuchToolError, "tool '#{name}' not found")
209
209
  end
210
210
 
211
211
  ##
@@ -122,7 +122,7 @@ class LLM::Tool
122
122
 
123
123
  ##
124
124
  # requires test-cmd.rb
125
- LLM.require "test-cmd.rb", "~> 2.7.1"
125
+ LLM.require "test-cmd.rb", "~> 2.7.3"
126
126
  Command = Test::Command
127
127
  end
128
128
  end
@@ -56,8 +56,12 @@ module LLM
56
56
  end
57
57
 
58
58
  ##
59
+ # @note An "ending" describes the method
60
+ # that matches the end of a tool call, and it can be one of:
61
+ # on_tool_finish, on_tool_error or on_tool_interrupt.
59
62
  # @param (see LLM::Tracer#on_tool_start)
60
- # @return [void]
63
+ # @return [LLM::Object]
64
+ # The span an ending is handed, with the call's id and name
61
65
  def on_tool_start(id:, name:, arguments:, model:, **)
62
66
  @logger.info(
63
67
  tracer: "llm.rb (logger)",
@@ -69,6 +73,7 @@ module LLM
69
73
  tool_arguments: arguments,
70
74
  model:
71
75
  )
76
+ LLM::Object.from(id:, name:)
72
77
  end
73
78
 
74
79
  ##
@@ -95,11 +100,47 @@ module LLM
95
100
  event: "tool.error",
96
101
  provider: provider_name,
97
102
  operation: "execute_tool",
103
+ error_class: ex.value[:type],
104
+ error_message: ex.value[:message]
105
+ )
106
+ end
107
+
108
+ ##
109
+ # @param (see LLM::Tracer#on_tool_interrupt)
110
+ # @return [void]
111
+ def on_tool_interrupt(ex:, span:, **)
112
+ @logger.info(
113
+ tracer: "llm.rb (logger)",
114
+ event: "tool.interrupt",
115
+ provider: provider_name,
116
+ operation: "execute_tool",
117
+ tool_id: span.id,
118
+ tool_name: span.name,
98
119
  error_class: ex.class.to_s,
99
120
  error_message: ex.message
100
121
  )
101
122
  end
102
123
 
124
+ ##
125
+ # @param (see LLM::Tracer#on_interrupt)
126
+ # @return [void]
127
+ def on_interrupt(scope:, request_id: nil, **)
128
+ @logger.info(
129
+ tracer: "llm.rb (logger)",
130
+ event: "#{scope}.interrupt",
131
+ provider: provider_name,
132
+ scope:,
133
+ request_id:
134
+ )
135
+ end
136
+
137
+ ##
138
+ # No-op.
139
+ # @return [nil]
140
+ def on_exit
141
+ nil
142
+ end
143
+
103
144
  private
104
145
 
105
146
  ##
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tracer
4
+ ##
5
+ # A tracer that fans out every tracing callback to a set of
6
+ # tracers, so a request can (for example) be logged to a file
7
+ # and exported to OpenTelemetry at the same time.
8
+ #
9
+ # {on_request_start} and {on_tool_start} return an array with
10
+ # one span per tracer, in the same order as {#tracers}. Pass the
11
+ # array back as `span:` to the matching finish, error, or
12
+ # interrupt callback, and each tracer is handed back the span
13
+ # it returned.
14
+ #
15
+ # A tracer that raises does not take the others down with it:
16
+ # the error is reported the same way
17
+ # {LLM::Tracer::Rescue LLM::Tracer::Rescue} reports a crashed
18
+ # tracer, and the remaining tracers still run.
19
+ #
20
+ # @example
21
+ # LLM::Tracer::Multi.new(
22
+ # LLM::Tracer.logger(llm, io: $stdout),
23
+ # LLM::Tracer.telemetry(llm)
24
+ # )
25
+ class Multi < self
26
+ ##
27
+ # Returns the tracer(s) under the supervision of Multi
28
+ # @return [Array<LLM::Tracer>]
29
+ attr_reader :tracers
30
+
31
+ ##
32
+ # @param [Array<LLM::Tracer>] tracers
33
+ # The tracers to fan out to. A Multi has no provider
34
+ # of its own.
35
+ def initialize(*tracers)
36
+ super(nil)
37
+ @tracers = tracers
38
+ end
39
+
40
+ ##
41
+ # @param (see LLM::Tracer#on_request_start)
42
+ # @return [Array]
43
+ # One span per tracer, in the same order as {#tracers}
44
+ def on_request_start(operation:, request_id:, model: nil, inputs: nil)
45
+ @tracers.map { _1.on_request_start(operation:, request_id:, model:, inputs:) }
46
+ end
47
+
48
+ ##
49
+ # @param (see LLM::Tracer#on_request_finish)
50
+ # @return [void]
51
+ def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
52
+ span.each.with_index do |s, i|
53
+ @tracers[i].on_request_finish(operation:, res:, model:, span: s, outputs:, metadata:, request_id:)
54
+ end
55
+ end
56
+
57
+ ##
58
+ # @param (see LLM::Tracer#on_request_error)
59
+ # @return [void]
60
+ def on_request_error(ex:, span:, request_id:)
61
+ span.each.with_index do |s, i|
62
+ @tracers[i].on_request_error(ex:, span: s, request_id:)
63
+ end
64
+ end
65
+
66
+ ##
67
+ # @param (see LLM::Tracer#on_tool_start)
68
+ # @return [Array]
69
+ # One span per tracer, in the same order as {#tracers}
70
+ def on_tool_start(id:, name:, arguments:, model:)
71
+ @tracers.map { _1.on_tool_start(id:, name:, arguments:, model:) }
72
+ end
73
+
74
+ ##
75
+ # @param (see LLM::Tracer#on_tool_interrupt)
76
+ # @return [void]
77
+ def on_tool_interrupt(ex:, span:, started_at:, finished_at:)
78
+ span.each.with_index do |s, i|
79
+ @tracers[i].on_tool_interrupt(ex:, span: s, started_at:, finished_at:)
80
+ end
81
+ end
82
+
83
+ ##
84
+ # @param (see LLM::Tracer#on_tool_finish)
85
+ # @return [void]
86
+ def on_tool_finish(result:, span:, started_at:, finished_at:)
87
+ span.each.with_index do |s, i|
88
+ @tracers[i].on_tool_finish(result:, span: s, started_at:, finished_at:)
89
+ end
90
+ end
91
+
92
+ ##
93
+ # @param (see LLM::Tracer#on_tool_error)
94
+ # @return [void]
95
+ def on_tool_error(ex:, span:, started_at:, finished_at:)
96
+ span.each.with_index do |s, i|
97
+ @tracers[i].on_tool_error(ex:, span: s, started_at:, finished_at:)
98
+ end
99
+ end
100
+
101
+ ##
102
+ # @param (see LLM::Tracer#on_interrupt)
103
+ # @return [void]
104
+ def on_interrupt(scope:, span: nil, request_id: nil)
105
+ spans = span || Array.new(@tracers.size)
106
+ spans.each.with_index do |s, i|
107
+ @tracers[i].on_interrupt(scope:, span: s, request_id:)
108
+ end
109
+ end
110
+
111
+ ##
112
+ # @return [void]
113
+ def on_exit
114
+ @tracers.each(&:on_exit)
115
+ end
116
+
117
+ ##
118
+ # @param (see LLM::Tracer#start_trace)
119
+ # @return [Array<LLM::Tracer>]
120
+ def start_trace(...)
121
+ @tracers.each { _1.start_trace(...) }
122
+ end
123
+
124
+ ##
125
+ # @return [Array<LLM::Tracer>]
126
+ def stop_trace
127
+ @tracers.each(&:stop_trace)
128
+ end
129
+
130
+ ##
131
+ # @param (see LLM::Tracer#set_finish_metadata_proc)
132
+ # @return [Array<LLM::Tracer>]
133
+ def set_finish_metadata_proc(...)
134
+ @tracers.each { _1.set_finish_metadata_proc(...) }
135
+ end
136
+
137
+ ##
138
+ # @param (see LLM::Tracer#merge_extra)
139
+ # @return [Array<LLM::Tracer>]
140
+ def merge_extra(...)
141
+ @tracers.each { _1.merge_extra(...) }
142
+ end
143
+ end
144
+ end
@@ -32,6 +32,13 @@ module LLM
32
32
  nil
33
33
  end
34
34
 
35
+ ##
36
+ # @param (see LLM::Tracer#on_tool_interrupt)
37
+ # @return [nil]
38
+ def on_tool_interrupt(**)
39
+ nil
40
+ end
41
+
35
42
  ##
36
43
  # @param (see LLM::Tracer#on_tool_finish)
37
44
  # @return [nil]
@@ -46,6 +53,19 @@ module LLM
46
53
  nil
47
54
  end
48
55
 
56
+ ##
57
+ # @param (see LLM::Tracer#on_interrupt)
58
+ # @return [nil]
59
+ def on_interrupt(**)
60
+ nil
61
+ end
62
+
63
+ ##
64
+ # @return [nil]
65
+ def on_exit
66
+ nil
67
+ end
68
+
49
69
  ##
50
70
  # @param (see LLM::Tracer#set_finish_metadata_proc)
51
71
  # @return [self]
@@ -52,9 +52,11 @@ module LLM
52
52
 
53
53
  ##
54
54
  # @param (see LLM::Tracer#on_tool_start)
55
- # @return [void]
55
+ # @return [LLM::Object]
56
+ # The span an ending is handed, with the call's id and name
56
57
  def on_tool_start(id:, name:, arguments:, **)
57
58
  @io.puts "#{timestamp} #{name}(#{format_arguments(arguments)})"
59
+ LLM::Object.from(id:, name:)
58
60
  end
59
61
 
60
62
  ##
@@ -68,7 +70,28 @@ module LLM
68
70
  # @param (see LLM::Tracer#on_tool_error)
69
71
  # @return [void]
70
72
  def on_tool_error(ex:, **)
71
- @io.puts "#{timestamp} tool error #{ex.class}: #{ex.message}"
73
+ @io.puts "#{timestamp} tool error #{ex.value[:type]}: #{ex.value[:message]}"
74
+ end
75
+
76
+ ##
77
+ # @param (see LLM::Tracer#on_tool_interrupt)
78
+ # @return [void]
79
+ def on_tool_interrupt(ex:, span:, **)
80
+ @io.puts "#{timestamp} tool #{span.name} (#{format_id(span.id)}) received an interrupt"
81
+ end
82
+
83
+ ##
84
+ # @param (see LLM::Tracer#on_interrupt)
85
+ # @return [void]
86
+ def on_interrupt(scope:, **)
87
+ @io.puts "#{timestamp} #{provider_name} interrupt received (scope=#{scope})"
88
+ end
89
+
90
+ ##
91
+ # No-op.
92
+ # @return [nil]
93
+ def on_exit
94
+ nil
72
95
  end
73
96
 
74
97
  private
@@ -123,5 +146,13 @@ module LLM
123
146
  str.size > max ? "#{str[0...max]}..." : str
124
147
  end
125
148
  end
149
+
150
+ ##
151
+ # A call id, which is a long string that reads as gibberish:
152
+ # ten characters are enough to tell two of them apart.
153
+ def format_id(id)
154
+ id = id.to_s
155
+ id.size > 10 ? "#{id[0, 10]}..." : id
156
+ end
126
157
  end
127
158
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Tracer
4
+ ##
5
+ # The {LLM::Tracer::Rescue} module prepends
6
+ # itself to every subclass of {LLM::Tracer}
7
+ # and ensures that a tracer callback never
8
+ # crashes an agent.
9
+ #
10
+ # When an error occurs, it is dumped to standard
11
+ # error and does not travel up the stack. But
12
+ # LLM::Interrupt, and any other exception not
13
+ # covered by the 'ERRORS' constant can crash
14
+ # the tracer. So it handles most cases but not
15
+ # all, and LLM::Interrupt travelling up the stack
16
+ # is intentional.
17
+ module Rescue
18
+ ##
19
+ # @param [StandardError] ex
20
+ # @return [void]
21
+ def self.crash(tracer, ex)
22
+ $stderr.puts "",
23
+ "an llm.rb tracer has crashed.",
24
+ "",
25
+ "[ tracer ] #{tracer.class}",
26
+ "[ class ] #{ex.class}",
27
+ "[ message ] #{ex.message}",
28
+ "[ backtrace ] ",
29
+ "#{(ex.backtrace || []).take(10).join("\n")}",
30
+ ""
31
+ end
32
+
33
+ ##
34
+ # @api private
35
+ ERRORS = [StandardError, ScriptError]
36
+ private_constant :ERRORS
37
+
38
+ def on_exit
39
+ super
40
+ rescue LLM::Interrupt
41
+ raise
42
+ rescue *ERRORS => ex
43
+ LLM::Tracer::Rescue.crash(self, ex)
44
+ end
45
+
46
+ def on_request_start(...)
47
+ super
48
+ rescue LLM::Interrupt
49
+ raise
50
+ rescue *ERRORS => ex
51
+ LLM::Tracer::Rescue.crash(self, ex)
52
+ end
53
+
54
+ def on_request_finish(...)
55
+ super
56
+ rescue LLM::Interrupt
57
+ raise
58
+ rescue *ERRORS => ex
59
+ LLM::Tracer::Rescue.crash(self, ex)
60
+ end
61
+
62
+ def on_request_error(...)
63
+ super
64
+ rescue LLM::Interrupt
65
+ raise
66
+ rescue *ERRORS => ex
67
+ LLM::Tracer::Rescue.crash(self, ex)
68
+ end
69
+
70
+ def on_tool_start(...)
71
+ super
72
+ rescue LLM::Interrupt
73
+ raise
74
+ rescue *ERRORS => ex
75
+ LLM::Tracer::Rescue.crash(self, ex)
76
+ end
77
+
78
+ def on_tool_interrupt(...)
79
+ super
80
+ rescue LLM::Interrupt
81
+ raise
82
+ rescue *ERRORS => ex
83
+ LLM::Tracer::Rescue.crash(self, ex)
84
+ end
85
+
86
+ def on_tool_finish(...)
87
+ super
88
+ rescue LLM::Interrupt
89
+ raise
90
+ rescue *ERRORS => ex
91
+ LLM::Tracer::Rescue.crash(self, ex)
92
+ end
93
+
94
+ def on_tool_error(...)
95
+ super
96
+ rescue LLM::Interrupt
97
+ raise
98
+ rescue *ERRORS => ex
99
+ LLM::Tracer::Rescue.crash(self, ex)
100
+ end
101
+
102
+ def on_interrupt(...)
103
+ super
104
+ rescue LLM::Interrupt
105
+ raise
106
+ rescue *ERRORS => ex
107
+ LLM::Tracer::Rescue.crash(self, ex)
108
+ end
109
+ end
110
+ end