llm.rb 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 357446a155ea5c66f1f1de5e2172f021bfa339c1006bae3f35a18b1c1ad173a7
4
- data.tar.gz: 4726be4c9133aa0da37771c6a16ba1eab27771acee6826a259f624a199cf8088
3
+ metadata.gz: c8252114d7ab58f00fd2d14389e932cfa9b2f0a71d96d9ea2c261f1f8b67d721
4
+ data.tar.gz: 8c00745ba750d0e271d8a4e5d5d9418a13556847c181e912aa8a8a1e7a9344b5
5
5
  SHA512:
6
- metadata.gz: b97ee9fc6594633d4176d21651a9625e8cd7c55d7d66d9e3a8a0bf5314df957447e7b4af431f57cd5e9c47408ceefc08babbb868af05d9ef7b887e543c6914a8
7
- data.tar.gz: 5881e618855cf3c9830fcc5edad571b2cb015a514ab99fcd4134e09402fda6ff51e7d034d50e96988739747a0dc5520b40c4938b9f1a0ed9af328dde22a48c99
6
+ metadata.gz: 9b45173644a3803db844c1cad679a14ec647e058bd4517ce0160b7f2f352480877375748a4abb727007d3c76e7b977a6c106a5bfa438575f69ef8cd60a5612d2
7
+ data.tar.gz: bebdb107819b1410bc6644529ad0f56544deadbae9661ab8ed2c84a420c74097b357113650de7ed7da1d0b388c601a9b7e60e30d887b62d72e7cfb84f3f0dd1a
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p align="center">
5
5
  <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
6
  <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
- <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.4.0-green.svg?" alt="Version"></a>
7
+ <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.5.0-green.svg?" alt="Version"></a>
8
8
  </p>
9
9
 
10
10
  ## About
@@ -177,11 +177,47 @@ end
177
177
  ses.talk(prompt)
178
178
  ```
179
179
 
180
+ #### Threads
181
+
182
+ llm.rb is designed for threaded environments with throughput in mind.
183
+ Locks are used selectively, and localized state is preferred wherever
184
+ possible. Blanket locking across every class would help guarantee
185
+ correctness but it would also add contention, reduce throughput,
186
+ and increase complexity.
187
+
188
+ That's why we decided to optimize for both correctness and throughput
189
+ instead. An important part of that design is guaranteeing that
190
+ [LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
191
+ is safe to share across threads. [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html) and
192
+ [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) are
193
+ stateful objects that should be kept local to a single thread. So the
194
+ recommended pattern is to keep one session or agent per thread,
195
+ and share a provider across multiple threads:
196
+
197
+ ```ruby
198
+ #!/usr/bin/env ruby
199
+ require "llm"
200
+
201
+ llm = LLM.openai(key: ENV["KEY"], persistent: true)
202
+ schema = llm.schema.object(answer: llm.schema.integer.required)
203
+
204
+ vals = 10.times.map do |x|
205
+ Thread.new do
206
+ ses = LLM::Session.new(llm, schema:)
207
+ res = ses.talk "#{x} + 5 = ?"
208
+ res.messages.find(&:assistant?).content!
209
+ end
210
+ end.map(&:value)
211
+
212
+ vals.each { |val| puts val }
213
+ ```
214
+
180
215
  ## Features
181
216
 
182
217
  #### General
183
218
  - ✅ Unified API across providers
184
219
  - 📦 Zero runtime deps (stdlib-only)
220
+ - 🧵 Thread-safe providers for multi-threaded workloads
185
221
  - 🧩 Pluggable JSON adapters (JSON, Oj, Yajl, etc)
186
222
  - 🧱 Builtin tracer API ([LLM::Tracer](https://0x1eef.github.io/x/llm.rb/LLM/Tracer.html))
187
223
 
@@ -438,18 +474,6 @@ ses2.restore(string: json)
438
474
  ses2.talk "Howdy partner. I'm back"
439
475
  ```
440
476
 
441
- #### Thread Safety
442
-
443
- The llm.rb library is thread-safe and can be used in a multi-threaded
444
- environments but it is important to keep in mind that the
445
- [LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
446
- and
447
- [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html)
448
- classes should be instantiated once per thread, and not shared
449
- between threads. Generally the library tries to avoid global or
450
- shared state but where it exists reentrant locks are used to
451
- ensure thread-safety.
452
-
453
477
  ### Tools
454
478
 
455
479
  #### LLM::Function
data/lib/llm/provider.rb CHANGED
@@ -36,10 +36,11 @@ class LLM::Provider
36
36
  @port = port
37
37
  @timeout = timeout
38
38
  @ssl = ssl
39
- @client = persistent ? persistent_client : transient_client
39
+ @client = persistent ? persistent_client : nil
40
40
  @tracer = LLM::Tracer::Null.new(self)
41
41
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
42
42
  @headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
43
+ @monitor = Monitor.new
43
44
  end
44
45
 
45
46
  ##
@@ -182,7 +183,7 @@ class LLM::Provider
182
183
  # Returns an object that can generate a JSON schema
183
184
  # @return [LLM::Schema]
184
185
  def schema
185
- @schema ||= LLM::Schema.new
186
+ LLM::Schema.new
186
187
  end
187
188
 
188
189
  ##
@@ -196,7 +197,9 @@ class LLM::Provider
196
197
  # @return [LLM::Provider]
197
198
  # Returns self
198
199
  def with(headers:)
199
- tap { @headers.merge!(headers) }
200
+ lock do
201
+ tap { @headers.merge!(headers) }
202
+ end
200
203
  end
201
204
 
202
205
  ##
@@ -277,10 +280,12 @@ class LLM::Provider
277
280
  # A tracer
278
281
  # @return [void]
279
282
  def tracer=(tracer)
280
- @tracer = if tracer.nil?
281
- LLM::Tracer::Null.new(self)
282
- else
283
- tracer
283
+ lock do
284
+ @tracer = if tracer.nil?
285
+ LLM::Tracer::Null.new(self)
286
+ else
287
+ tracer
288
+ end
284
289
  end
285
290
  end
286
291
 
@@ -336,10 +341,12 @@ class LLM::Provider
336
341
  # When there is a network error at the operating system level
337
342
  # @return [Net::HTTPResponse]
338
343
  def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
339
- span = @tracer.on_request_start(operation:, model:)
340
- args = (Net::HTTP === client) ? [request] : [URI.join(base_uri, request.path), request]
344
+ tracer = @tracer
345
+ span = tracer.on_request_start(operation:, model:)
346
+ http = client || transient_client
347
+ args = (Net::HTTP === http) ? [request] : [URI.join(base_uri, request.path), request]
341
348
  res = if stream
342
- client.request(*args) do |res|
349
+ http.request(*args) do |res|
343
350
  handler = event_handler.new stream_parser.new(stream)
344
351
  parser = LLM::EventStream::Parser.new
345
352
  parser.register(handler)
@@ -353,10 +360,10 @@ class LLM::Provider
353
360
  parser&.free
354
361
  end
355
362
  else
356
- b ? client.request(*args) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 } :
357
- client.request(*args)
363
+ b ? http.request(*args) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 } :
364
+ http.request(*args)
358
365
  end
359
- [handle_response(res, span), span]
366
+ [handle_response(res, tracer, span), span, tracer]
360
367
  end
361
368
 
362
369
  ##
@@ -366,14 +373,18 @@ class LLM::Provider
366
373
  # @param [Object, nil] span
367
374
  # The span
368
375
  # @return [Net::HTTPResponse]
369
- def handle_response(res, span)
376
+ def handle_response(res, tracer, span)
370
377
  case res
371
378
  when Net::HTTPOK then res.body = parse_response(res)
372
- else error_handler.new(@tracer, span, res).raise_error!
379
+ else error_handler.new(tracer, span, res).raise_error!
373
380
  end
374
381
  res
375
382
  end
376
383
 
384
+ ##
385
+ # Parse a HTTP response
386
+ # @param [Net::HTTPResponse] res
387
+ # @return [LLM::Object, String]
377
388
  def parse_response(res)
378
389
  case res["content-type"]
379
390
  when %r|\Aapplication/json\s*| then LLM::Object.from(LLM.json.load(res.body))
@@ -418,14 +429,8 @@ class LLM::Provider
418
429
  end
419
430
 
420
431
  ##
421
- # Finalizes tracing after a response has been adapted/wrapped.
422
- # @param [String] operation
423
- # @param [String, nil] model
424
- # @param [LLM::Response] res
425
- # @param [Object, nil] span
426
- # @return [LLM::Response]
427
- def finish_trace(operation:, res:, model: nil, span: nil)
428
- @tracer.on_request_finish(operation:, model:, res:, span:)
429
- res
432
+ # @api private
433
+ def lock(&)
434
+ @monitor.synchronize(&)
430
435
  end
431
436
  end
@@ -38,9 +38,10 @@ class LLM::Anthropic
38
38
  def all(**params)
39
39
  query = URI.encode_www_form(params)
40
40
  req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
41
- res, span = execute(request: req, operation: "request")
41
+ res, span, tracer = execute(request: req, operation: "request")
42
42
  res = ResponseAdapter.adapt(res, type: :enumerable)
43
- finish_trace(operation: "request", res:, span:)
43
+ tracer.on_request_finish(operation: "request", res:, span:)
44
+ res
44
45
  end
45
46
 
46
47
  ##
@@ -58,9 +59,10 @@ class LLM::Anthropic
58
59
  req = Net::HTTP::Post.new("/v1/files", headers)
59
60
  req["content-type"] = multi.content_type
60
61
  set_body_stream(req, multi.body)
61
- res, span = execute(request: req, operation: "request")
62
+ res, span, tracer = execute(request: req, operation: "request")
62
63
  res = ResponseAdapter.adapt(res, type: :file)
63
- finish_trace(operation: "request", res:, span:)
64
+ tracer.on_request_finish(operation: "request", res:, span:)
65
+ res
64
66
  end
65
67
 
66
68
  ##
@@ -78,9 +80,10 @@ class LLM::Anthropic
78
80
  file_id = file.respond_to?(:id) ? file.id : file
79
81
  query = URI.encode_www_form(params)
80
82
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
81
- res, span = execute(request: req, operation: "request")
83
+ res, span, tracer = execute(request: req, operation: "request")
82
84
  res = ResponseAdapter.adapt(res, type: :file)
83
- finish_trace(operation: "request", res:, span:)
85
+ tracer.on_request_finish(operation: "request", res:, span:)
86
+ res
84
87
  end
85
88
 
86
89
  ##
@@ -98,9 +101,10 @@ class LLM::Anthropic
98
101
  query = URI.encode_www_form(params)
99
102
  file_id = file.respond_to?(:id) ? file.id : file
100
103
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
101
- res, span = execute(request: req, operation: "request")
104
+ res, span, tracer = execute(request: req, operation: "request")
102
105
  res = ResponseAdapter.adapt(res, type: :file)
103
- finish_trace(operation: "request", res:, span:)
106
+ tracer.on_request_finish(operation: "request", res:, span:)
107
+ res
104
108
  end
105
109
  alias_method :retrieve_metadata, :get_metadata
106
110
 
@@ -117,9 +121,10 @@ class LLM::Anthropic
117
121
  def delete(file:)
118
122
  file_id = file.respond_to?(:id) ? file.id : file
119
123
  req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
120
- res, span = execute(request: req, operation: "request")
124
+ res, span, tracer = execute(request: req, operation: "request")
121
125
  res = LLM::Response.new(res)
122
- finish_trace(operation: "request", res:, span:)
126
+ tracer.on_request_finish(operation: "request", res:, span:)
127
+ res
123
128
  end
124
129
 
125
130
  ##
@@ -142,9 +147,10 @@ class LLM::Anthropic
142
147
  file_id = file.respond_to?(:id) ? file.id : file
143
148
  req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
144
149
  io = StringIO.new("".b)
145
- res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
150
+ res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
146
151
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
147
- finish_trace(operation: "request", res:, span:)
152
+ tracer.on_request_finish(operation: "request", res:, span:)
153
+ res
148
154
  end
149
155
 
150
156
  private
@@ -153,7 +159,7 @@ class LLM::Anthropic
153
159
  @provider.instance_variable_get(:@key)
154
160
  end
155
161
 
156
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
162
+ [:headers, :execute, :set_body_stream].each do |m|
157
163
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
158
164
  end
159
165
  end
@@ -40,14 +40,15 @@ class LLM::Anthropic
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
42
  req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
43
- res, span = execute(request: req, operation: "request")
43
+ res, span, tracer = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :enumerable)
45
- finish_trace(operation: "request", res:, span:)
45
+ tracer.on_request_finish(operation: "request", res:, span:)
46
+ res
46
47
  end
47
48
 
48
49
  private
49
50
 
50
- [:headers, :execute, :finish_trace].each do |m|
51
+ [:headers, :execute].each do |m|
51
52
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
52
53
  end
53
54
  end
@@ -43,10 +43,11 @@ module LLM
43
43
  def complete(prompt, params = {})
44
44
  params, stream, tools, role = normalize_complete_params(params)
45
45
  req = build_complete_request(prompt, params, role)
46
- res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
46
+ res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
47
47
  res = ResponseAdapter.adapt(res, type: :completion)
48
48
  .extend(Module.new { define_method(:__tools__) { tools } })
49
- finish_trace(operation: "chat", model: params[:model], res:, span:)
49
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
50
+ res
50
51
  end
51
52
 
52
53
  ##
@@ -110,12 +111,14 @@ module LLM
110
111
  private
111
112
 
112
113
  def headers
113
- (@headers || {}).merge(
114
- "Content-Type" => "application/json",
115
- "x-api-key" => @key,
116
- "anthropic-version" => "2023-06-01",
117
- "anthropic-beta" => "files-api-2025-04-14"
118
- )
114
+ lock do
115
+ (@headers || {}).merge(
116
+ "Content-Type" => "application/json",
117
+ "x-api-key" => @key,
118
+ "anthropic-version" => "2023-06-01",
119
+ "anthropic-beta" => "files-api-2025-04-14"
120
+ )
121
+ end
119
122
  end
120
123
 
121
124
  def stream_parser
@@ -46,9 +46,10 @@ class LLM::Gemini
46
46
  def all(**params)
47
47
  query = URI.encode_www_form(params.merge!(key: key))
48
48
  req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
49
- res, span = execute(request: req, operation: "request")
49
+ res, span, tracer = execute(request: req, operation: "request")
50
50
  res = ResponseAdapter.adapt(res, type: :files)
51
- finish_trace(operation: "request", res:, span:)
51
+ tracer.on_request_finish(operation: "request", res:, span:)
52
+ res
52
53
  end
53
54
 
54
55
  ##
@@ -69,9 +70,10 @@ class LLM::Gemini
69
70
  req["X-Goog-Upload-Command"] = "upload, finalize"
70
71
  file.with_io do |io|
71
72
  set_body_stream(req, io)
72
- res, span = execute(request: req, operation: "request")
73
+ res, span, tracer = execute(request: req, operation: "request")
73
74
  res = ResponseAdapter.adapt(res, type: :file)
74
- finish_trace(operation: "request", res:, span:)
75
+ tracer.on_request_finish(operation: "request", res:, span:)
76
+ res
75
77
  end
76
78
  end
77
79
 
@@ -90,9 +92,10 @@ class LLM::Gemini
90
92
  file_id = file.respond_to?(:name) ? file.name : file.to_s
91
93
  query = URI.encode_www_form(params.merge!(key: key))
92
94
  req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
93
- res, span = execute(request: req, operation: "request")
95
+ res, span, tracer = execute(request: req, operation: "request")
94
96
  res = ResponseAdapter.adapt(res, type: :file)
95
- finish_trace(operation: "request", res:, span:)
97
+ tracer.on_request_finish(operation: "request", res:, span:)
98
+ res
96
99
  end
97
100
 
98
101
  ##
@@ -109,9 +112,10 @@ class LLM::Gemini
109
112
  file_id = file.respond_to?(:name) ? file.name : file.to_s
110
113
  query = URI.encode_www_form(params.merge!(key: key))
111
114
  req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
112
- res, span = execute(request: req, operation: "request")
115
+ res, span, tracer = execute(request: req, operation: "request")
113
116
  res = LLM::Response.new(res)
114
- finish_trace(operation: "request", res:, span:)
117
+ tracer.on_request_finish(operation: "request", res:, span:)
118
+ res
115
119
  end
116
120
 
117
121
  ##
@@ -132,8 +136,9 @@ class LLM::Gemini
132
136
  req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
133
137
  req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
134
138
  req.body = LLM.json.dump({file: {display_name: File.basename(file.path)}})
135
- res, span = execute(request: req, operation: "request")
136
- finish_trace(operation: "request", res: LLM::Response.new(res), span:)
139
+ res, span, tracer = execute(request: req, operation: "request")
140
+ trace_res = LLM::Response.new(res)
141
+ tracer.on_request_finish(operation: "request", res: trace_res, span:)
137
142
  res["x-goog-upload-url"]
138
143
  end
139
144
 
@@ -141,7 +146,7 @@ class LLM::Gemini
141
146
  @provider.instance_variable_get(:@key)
142
147
  end
143
148
 
144
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
149
+ [:headers, :execute, :set_body_stream].each do |m|
145
150
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
146
151
  end
147
152
  end
@@ -51,9 +51,10 @@ class LLM::Gemini
51
51
  instances: [{prompt:}]
52
52
  })
53
53
  req.body = body
54
- res, span = execute(request: req, operation: "request")
54
+ res, span, tracer = execute(request: req, operation: "request")
55
55
  res = ResponseAdapter.adapt(res, type: :image)
56
- finish_trace(operation: "request", model:, res:, span:)
56
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
57
+ res
57
58
  end
58
59
 
59
60
  ##
@@ -90,7 +91,7 @@ class LLM::Gemini
90
91
  @provider.instance_variable_get(:@key)
91
92
  end
92
93
 
93
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
94
+ [:headers, :execute, :set_body_stream].each do |m|
94
95
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
95
96
  end
96
97
  end
@@ -42,9 +42,10 @@ class LLM::Gemini
42
42
  def all(**params)
43
43
  query = URI.encode_www_form(params.merge!(key: key))
44
44
  req = Net::HTTP::Get.new("/v1beta/models?#{query}", headers)
45
- res, span = execute(request: req, operation: "request")
45
+ res, span, tracer = execute(request: req, operation: "request")
46
46
  res = ResponseAdapter.adapt(res, type: :models)
47
- finish_trace(operation: "request", res:, span:)
47
+ tracer.on_request_finish(operation: "request", res:, span:)
48
+ res
48
49
  end
49
50
 
50
51
  private
@@ -53,7 +54,7 @@ class LLM::Gemini
53
54
  @provider.instance_variable_get(:@key)
54
55
  end
55
56
 
56
- [:headers, :execute, :finish_trace].each do |m|
57
+ [:headers, :execute].each do |m|
57
58
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
58
59
  end
59
60
  end
@@ -48,7 +48,7 @@ module LLM::Gemini::RequestAdapter
48
48
  def adapt_object(object)
49
49
  case object.kind
50
50
  when :image_url
51
- [{file_data: {mime_type: "image/*", file_uri: object.value.to_s}}]
51
+ [{file_data: {file_uri: object.value.to_s}}]
52
52
  when :local_file
53
53
  file = object.value
54
54
  [{inline_data: {mime_type: file.mime_type, data: file.to_b64}}]
@@ -49,9 +49,10 @@ module LLM
49
49
  path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
50
50
  req = Net::HTTP::Post.new(path, headers)
51
51
  req.body = LLM.json.dump({content: {parts: [{text: input}]}})
52
- res, span = execute(request: req, operation: "embeddings", model:)
52
+ res, span, tracer = execute(request: req, operation: "embeddings", model:)
53
53
  res = ResponseAdapter.adapt(res, type: :embedding)
54
- finish_trace(operation: "embeddings", model:, res:, span:)
54
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
55
+ res
55
56
  end
56
57
 
57
58
  ##
@@ -67,10 +68,11 @@ module LLM
67
68
  def complete(prompt, params = {})
68
69
  params, stream, tools, role, model = normalize_complete_params(params)
69
70
  req = build_complete_request(prompt, params, role, model, stream)
70
- res, span = execute(request: req, stream: stream, operation: "chat", model:)
71
+ res, span, tracer = execute(request: req, stream: stream, operation: "chat", model:)
71
72
  res = ResponseAdapter.adapt(res, type: :completion)
72
73
  .extend(Module.new { define_method(:__tools__) { tools } })
73
- finish_trace(operation: "chat", model:, res:, span:)
74
+ tracer.on_request_finish(operation: "chat", model:, res:, span:)
75
+ res
74
76
  end
75
77
 
76
78
  ##
@@ -167,9 +169,11 @@ module LLM
167
169
  private
168
170
 
169
171
  def headers
170
- (@headers || {}).merge(
171
- "Content-Type" => "application/json"
172
- )
172
+ lock do
173
+ (@headers || {}).merge(
174
+ "Content-Type" => "application/json"
175
+ )
176
+ end
173
177
  end
174
178
 
175
179
  def stream_parser
@@ -43,14 +43,15 @@ class LLM::Ollama
43
43
  def all(**params)
44
44
  query = URI.encode_www_form(params)
45
45
  req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
46
- res, span = execute(request: req, operation: "request")
46
+ res, span, tracer = execute(request: req, operation: "request")
47
47
  res = LLM::Response.new(res)
48
- finish_trace(operation: "request", res:, span:)
48
+ tracer.on_request_finish(operation: "request", res:, span:)
49
+ res
49
50
  end
50
51
 
51
52
  private
52
53
 
53
- [:headers, :execute, :finish_trace].each do |m|
54
+ [:headers, :execute].each do |m|
54
55
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
55
56
  end
56
57
  end
@@ -43,9 +43,10 @@ module LLM
43
43
  params = {model:}.merge!(params)
44
44
  req = Net::HTTP::Post.new("/v1/embeddings", headers)
45
45
  req.body = LLM.json.dump({input:}.merge!(params))
46
- res, span = execute(request: req, operation: "embeddings", model:)
46
+ res, span, tracer = execute(request: req, operation: "embeddings", model:)
47
47
  res = ResponseAdapter.adapt(res, type: :embedding)
48
- finish_trace(operation: "embeddings", model:, res:, span:)
48
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
49
+ res
49
50
  end
50
51
 
51
52
  ##
@@ -61,10 +62,11 @@ module LLM
61
62
  def complete(prompt, params = {})
62
63
  params, stream, tools, role = normalize_complete_params(params)
63
64
  req = build_complete_request(prompt, params, role)
64
- res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
65
+ res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
65
66
  res = ResponseAdapter.adapt(res, type: :completion)
66
67
  .extend(Module.new { define_method(:__tools__) { tools } })
67
- finish_trace(operation: "chat", model: params[:model], res:, span:)
68
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
69
+ res
68
70
  end
69
71
 
70
72
  ##
@@ -92,10 +94,12 @@ module LLM
92
94
  private
93
95
 
94
96
  def headers
95
- (@headers || {}).merge(
96
- "Content-Type" => "application/json",
97
- "Authorization" => "Bearer #{@key}"
98
- )
97
+ lock do
98
+ (@headers || {}).merge(
99
+ "Content-Type" => "application/json",
100
+ "Authorization" => "Bearer #{@key}"
101
+ )
102
+ end
99
103
  end
100
104
 
101
105
  def stream_parser
@@ -35,9 +35,10 @@ class LLM::OpenAI
35
35
  req = Net::HTTP::Post.new("/v1/audio/speech", headers)
36
36
  req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
37
37
  io = StringIO.new("".b)
38
- res, span = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
38
+ res, span, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
39
39
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
40
- finish_trace(operation: "request", model:, res:, span:)
40
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
41
+ res
41
42
  end
42
43
 
43
44
  ##
@@ -57,9 +58,10 @@ class LLM::OpenAI
57
58
  req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
58
59
  req["content-type"] = multi.content_type
59
60
  set_body_stream(req, multi.body)
60
- res, span = execute(request: req, operation: "request")
61
+ res, span, tracer = execute(request: req, operation: "request")
61
62
  res = LLM::Response.new(res)
62
- finish_trace(operation: "request", model:, res:, span:)
63
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
64
+ res
63
65
  end
64
66
 
65
67
  ##
@@ -80,14 +82,15 @@ class LLM::OpenAI
80
82
  req = Net::HTTP::Post.new("/v1/audio/translations", headers)
81
83
  req["content-type"] = multi.content_type
82
84
  set_body_stream(req, multi.body)
83
- res, span = execute(request: req, operation: "request")
85
+ res, span, tracer = execute(request: req, operation: "request")
84
86
  res = LLM::Response.new(res)
85
- finish_trace(operation: "request", model:, res:, span:)
87
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
88
+ res
86
89
  end
87
90
 
88
91
  private
89
92
 
90
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
93
+ [:headers, :execute, :set_body_stream].each do |m|
91
94
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
92
95
  end
93
96
  end
@@ -41,9 +41,10 @@ class LLM::OpenAI
41
41
  def all(**params)
42
42
  query = URI.encode_www_form(params)
43
43
  req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
44
- res, span = execute(request: req, operation: "request")
44
+ res, span, tracer = execute(request: req, operation: "request")
45
45
  res = ResponseAdapter.adapt(res, type: :enumerable)
46
- finish_trace(operation: "request", res:, span:)
46
+ tracer.on_request_finish(operation: "request", res:, span:)
47
+ res
47
48
  end
48
49
 
49
50
  ##
@@ -62,9 +63,10 @@ class LLM::OpenAI
62
63
  req = Net::HTTP::Post.new("/v1/files", headers)
63
64
  req["content-type"] = multi.content_type
64
65
  set_body_stream(req, multi.body)
65
- res, span = execute(request: req, operation: "request")
66
+ res, span, tracer = execute(request: req, operation: "request")
66
67
  res = ResponseAdapter.adapt(res, type: :file)
67
- finish_trace(operation: "request", res:, span:)
68
+ tracer.on_request_finish(operation: "request", res:, span:)
69
+ res
68
70
  end
69
71
 
70
72
  ##
@@ -82,9 +84,10 @@ class LLM::OpenAI
82
84
  file_id = file.respond_to?(:id) ? file.id : file
83
85
  query = URI.encode_www_form(params)
84
86
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
85
- res, span = execute(request: req, operation: "request")
87
+ res, span, tracer = execute(request: req, operation: "request")
86
88
  res = ResponseAdapter.adapt(res, type: :file)
87
- finish_trace(operation: "request", res:, span:)
89
+ tracer.on_request_finish(operation: "request", res:, span:)
90
+ res
88
91
  end
89
92
 
90
93
  ##
@@ -104,9 +107,10 @@ class LLM::OpenAI
104
107
  file_id = file.respond_to?(:id) ? file.id : file
105
108
  req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
106
109
  io = StringIO.new("".b)
107
- res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
110
+ res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
108
111
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
109
- finish_trace(operation: "request", res:, span:)
112
+ tracer.on_request_finish(operation: "request", res:, span:)
113
+ res
110
114
  end
111
115
 
112
116
  ##
@@ -122,14 +126,15 @@ class LLM::OpenAI
122
126
  def delete(file:)
123
127
  file_id = file.respond_to?(:id) ? file.id : file
124
128
  req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
125
- res, span = execute(request: req, operation: "request")
129
+ res, span, tracer = execute(request: req, operation: "request")
126
130
  res = LLM::Response.new(res)
127
- finish_trace(operation: "request", res:, span:)
131
+ tracer.on_request_finish(operation: "request", res:, span:)
132
+ res
128
133
  end
129
134
 
130
135
  private
131
136
 
132
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
137
+ [:headers, :execute, :set_body_stream].each do |m|
133
138
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
134
139
  end
135
140
  end
@@ -50,9 +50,10 @@ class LLM::OpenAI
50
50
  def create(prompt:, model: "dall-e-3", **params)
51
51
  req = Net::HTTP::Post.new("/v1/images/generations", headers)
52
52
  req.body = LLM.json.dump({prompt:, n: 1, model:}.merge!(params))
53
- res, span = execute(request: req, operation: "request")
53
+ res, span, tracer = execute(request: req, operation: "request")
54
54
  res = ResponseAdapter.adapt(res, type: :image)
55
- finish_trace(operation: "request", model:, res:, span:)
55
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
56
+ res
56
57
  end
57
58
 
58
59
  ##
@@ -73,9 +74,10 @@ class LLM::OpenAI
73
74
  req = Net::HTTP::Post.new("/v1/images/variations", headers)
74
75
  req["content-type"] = multi.content_type
75
76
  set_body_stream(req, multi.body)
76
- res, span = execute(request: req, operation: "request")
77
+ res, span, tracer = execute(request: req, operation: "request")
77
78
  res = ResponseAdapter.adapt(res, type: :image)
78
- finish_trace(operation: "request", model:, res:, span:)
79
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
80
+ res
79
81
  end
80
82
 
81
83
  ##
@@ -97,14 +99,15 @@ class LLM::OpenAI
97
99
  req = Net::HTTP::Post.new("/v1/images/edits", headers)
98
100
  req["content-type"] = multi.content_type
99
101
  set_body_stream(req, multi.body)
100
- res, span = execute(request: req, operation: "request")
102
+ res, span, tracer = execute(request: req, operation: "request")
101
103
  res = ResponseAdapter.adapt(res, type: :image)
102
- finish_trace(operation: "request", model:, res:, span:)
104
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
105
+ res
103
106
  end
104
107
 
105
108
  private
106
109
 
107
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
110
+ [:headers, :execute, :set_body_stream].each do |m|
108
111
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
109
112
  end
110
113
  end
@@ -40,14 +40,15 @@ class LLM::OpenAI
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
42
  req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
43
- res, span = execute(request: req, operation: "request")
43
+ res, span, tracer = execute(request: req, operation: "request")
44
44
  res = ResponseAdapter.adapt(res, type: :enumerable)
45
- finish_trace(operation: "request", res:, span:)
45
+ tracer.on_request_finish(operation: "request", res:, span:)
46
+ res
46
47
  end
47
48
 
48
49
  private
49
50
 
50
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
51
+ [:headers, :execute, :set_body_stream].each do |m|
51
52
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
52
53
  end
53
54
  end
@@ -50,14 +50,15 @@ class LLM::OpenAI
50
50
  req = Net::HTTP::Post.new("/v1/moderations", headers)
51
51
  input = RequestAdapter::Moderation.new(input).adapt
52
52
  req.body = LLM.json.dump({input:, model:}.merge!(params))
53
- res, span = execute(request: req, operation: "request")
53
+ res, span, tracer = execute(request: req, operation: "request")
54
54
  res = ResponseAdapter.adapt(res, type: :moderations)
55
- finish_trace(operation: "request", model:, res:, span:)
55
+ tracer.on_request_finish(operation: "request", model:, res:, span:)
56
+ res
56
57
  end
57
58
 
58
59
  private
59
60
 
60
- [:headers, :execute, :finish_trace].each do |m|
61
+ [:headers, :execute].each do |m|
61
62
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
62
63
  end
63
64
  end
@@ -44,10 +44,11 @@ class LLM::OpenAI
44
44
  messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
45
45
  body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
46
46
  set_body_stream(req, StringIO.new(body))
47
- res, span = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
47
+ res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
48
48
  res = ResponseAdapter.adapt(res, type: :responds)
49
49
  .extend(Module.new { define_method(:__tools__) { tools } })
50
- finish_trace(operation: "chat", model: params[:model], res:, span:)
50
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
51
+ res
51
52
  end
52
53
 
53
54
  ##
@@ -60,9 +61,10 @@ class LLM::OpenAI
60
61
  response_id = response.respond_to?(:id) ? response.id : response
61
62
  query = URI.encode_www_form(params)
62
63
  req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
63
- res, span = execute(request: req, operation: "request")
64
+ res, span, tracer = execute(request: req, operation: "request")
64
65
  res = ResponseAdapter.adapt(res, type: :responds)
65
- finish_trace(operation: "request", res:, span:)
66
+ tracer.on_request_finish(operation: "request", res:, span:)
67
+ res
66
68
  end
67
69
 
68
70
  ##
@@ -74,14 +76,15 @@ class LLM::OpenAI
74
76
  def delete(response)
75
77
  response_id = response.respond_to?(:id) ? response.id : response
76
78
  req = Net::HTTP::Delete.new("/v1/responses/#{response_id}", headers)
77
- res, span = execute(request: req, operation: "request")
79
+ res, span, tracer = execute(request: req, operation: "request")
78
80
  res = LLM::Response.new(res)
79
- finish_trace(operation: "request", res:, span:)
81
+ tracer.on_request_finish(operation: "request", res:, span:)
82
+ res
80
83
  end
81
84
 
82
85
  private
83
86
 
84
- [:headers, :execute, :set_body_stream, :resolve_tools, :finish_trace].each do |m|
87
+ [:headers, :execute, :set_body_stream, :resolve_tools].each do |m|
85
88
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
86
89
  end
87
90
 
@@ -32,9 +32,10 @@ class LLM::OpenAI
32
32
  def all(**params)
33
33
  query = URI.encode_www_form(params)
34
34
  req = Net::HTTP::Get.new("/v1/vector_stores?#{query}", headers)
35
- res, span = execute(request: req, operation: "request")
35
+ res, span, tracer = execute(request: req, operation: "request")
36
36
  res = ResponseAdapter.adapt(res, type: :enumerable)
37
- finish_trace(operation: "request", res:, span:)
37
+ tracer.on_request_finish(operation: "request", res:, span:)
38
+ res
38
39
  end
39
40
 
40
41
  ##
@@ -48,9 +49,10 @@ class LLM::OpenAI
48
49
  def create(name:, file_ids: nil, **params)
49
50
  req = Net::HTTP::Post.new("/v1/vector_stores", headers)
50
51
  req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
51
- res, span = execute(request: req, operation: "request")
52
+ res, span, tracer = execute(request: req, operation: "request")
52
53
  res = LLM::Response.new(res)
53
- finish_trace(operation: "request", res:, span:)
54
+ tracer.on_request_finish(operation: "request", res:, span:)
55
+ res
54
56
  end
55
57
 
56
58
  ##
@@ -71,9 +73,10 @@ class LLM::OpenAI
71
73
  def get(vector:)
72
74
  vector_id = vector.respond_to?(:id) ? vector.id : vector
73
75
  req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}", headers)
74
- res, span = execute(request: req, operation: "request")
76
+ res, span, tracer = execute(request: req, operation: "request")
75
77
  res = LLM::Response.new(res)
76
- finish_trace(operation: "request", res:, span:)
78
+ tracer.on_request_finish(operation: "request", res:, span:)
79
+ res
77
80
  end
78
81
 
79
82
  ##
@@ -88,9 +91,10 @@ class LLM::OpenAI
88
91
  vector_id = vector.respond_to?(:id) ? vector.id : vector
89
92
  req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}", headers)
90
93
  req.body = LLM.json.dump(params.merge({name:}).compact)
91
- res, span = execute(request: req, operation: "request")
94
+ res, span, tracer = execute(request: req, operation: "request")
92
95
  res = LLM::Response.new(res)
93
- finish_trace(operation: "request", res:, span:)
96
+ tracer.on_request_finish(operation: "request", res:, span:)
97
+ res
94
98
  end
95
99
 
96
100
  ##
@@ -102,9 +106,10 @@ class LLM::OpenAI
102
106
  def delete(vector:)
103
107
  vector_id = vector.respond_to?(:id) ? vector.id : vector
104
108
  req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}", headers)
105
- res, span = execute(request: req, operation: "request")
109
+ res, span, tracer = execute(request: req, operation: "request")
106
110
  res = LLM::Response.new(res)
107
- finish_trace(operation: "request", res:, span:)
111
+ tracer.on_request_finish(operation: "request", res:, span:)
112
+ res
108
113
  end
109
114
 
110
115
  ##
@@ -119,9 +124,10 @@ class LLM::OpenAI
119
124
  vector_id = vector.respond_to?(:id) ? vector.id : vector
120
125
  req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/search", headers)
121
126
  req.body = LLM.json.dump(params.merge({query:}).compact)
122
- res, span = execute(request: req, operation: "retrieval")
127
+ res, span, tracer = execute(request: req, operation: "retrieval")
123
128
  res = ResponseAdapter.adapt(res, type: :enumerable)
124
- finish_trace(operation: "retrieval", res:, span:)
129
+ tracer.on_request_finish(operation: "retrieval", res:, span:)
130
+ res
125
131
  end
126
132
 
127
133
  ##
@@ -135,9 +141,10 @@ class LLM::OpenAI
135
141
  vector_id = vector.respond_to?(:id) ? vector.id : vector
136
142
  query = URI.encode_www_form(params)
137
143
  req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files?#{query}", headers)
138
- res, span = execute(request: req, operation: "request")
144
+ res, span, tracer = execute(request: req, operation: "request")
139
145
  res = ResponseAdapter.adapt(res, type: :enumerable)
140
- finish_trace(operation: "request", res:, span:)
146
+ tracer.on_request_finish(operation: "request", res:, span:)
147
+ res
141
148
  end
142
149
 
143
150
  ##
@@ -154,9 +161,10 @@ class LLM::OpenAI
154
161
  file_id = file.respond_to?(:id) ? file.id : file
155
162
  req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files", headers)
156
163
  req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
157
- res, span = execute(request: req, operation: "request")
164
+ res, span, tracer = execute(request: req, operation: "request")
158
165
  res = LLM::Response.new(res)
159
- finish_trace(operation: "request", res:, span:)
166
+ tracer.on_request_finish(operation: "request", res:, span:)
167
+ res
160
168
  end
161
169
  alias_method :create_file, :add_file
162
170
 
@@ -184,9 +192,10 @@ class LLM::OpenAI
184
192
  file_id = file.respond_to?(:id) ? file.id : file
185
193
  req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
186
194
  req.body = LLM.json.dump(params.merge({attributes:}).compact)
187
- res, span = execute(request: req, operation: "request")
195
+ res, span, tracer = execute(request: req, operation: "request")
188
196
  res = LLM::Response.new(res)
189
- finish_trace(operation: "request", res:, span:)
197
+ tracer.on_request_finish(operation: "request", res:, span:)
198
+ res
190
199
  end
191
200
 
192
201
  ##
@@ -201,9 +210,10 @@ class LLM::OpenAI
201
210
  file_id = file.respond_to?(:id) ? file.id : file
202
211
  query = URI.encode_www_form(params)
203
212
  req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files/#{file_id}?#{query}", headers)
204
- res, span = execute(request: req, operation: "request")
213
+ res, span, tracer = execute(request: req, operation: "request")
205
214
  res = LLM::Response.new(res)
206
- finish_trace(operation: "request", res:, span:)
215
+ tracer.on_request_finish(operation: "request", res:, span:)
216
+ res
207
217
  end
208
218
 
209
219
  ##
@@ -217,9 +227,10 @@ class LLM::OpenAI
217
227
  vector_id = vector.respond_to?(:id) ? vector.id : vector
218
228
  file_id = file.respond_to?(:id) ? file.id : file
219
229
  req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
220
- res, span = execute(request: req, operation: "request")
230
+ res, span, tracer = execute(request: req, operation: "request")
221
231
  res = LLM::Response.new(res)
222
- finish_trace(operation: "request", res:, span:)
232
+ tracer.on_request_finish(operation: "request", res:, span:)
233
+ res
223
234
  end
224
235
 
225
236
  ##
@@ -248,7 +259,7 @@ class LLM::OpenAI
248
259
 
249
260
  private
250
261
 
251
- [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
262
+ [:headers, :execute, :set_body_stream].each do |m|
252
263
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
253
264
  end
254
265
  end
@@ -47,9 +47,10 @@ module LLM
47
47
  def embed(input, model: "text-embedding-3-small", **params)
48
48
  req = Net::HTTP::Post.new("/v1/embeddings", headers)
49
49
  req.body = LLM.json.dump({input:, model:}.merge!(params))
50
- res, span = execute(request: req, operation: "embeddings", model:)
50
+ res, span, tracer = execute(request: req, operation: "embeddings", model:)
51
51
  res = ResponseAdapter.adapt(res, type: :embedding)
52
- finish_trace(operation: "embeddings", model:, res:, span:)
52
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
53
+ res
53
54
  end
54
55
 
55
56
  ##
@@ -65,10 +66,11 @@ module LLM
65
66
  def complete(prompt, params = {})
66
67
  params, stream, tools, role = normalize_complete_params(params)
67
68
  req = build_complete_request(prompt, params, role)
68
- res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
69
+ res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
69
70
  res = ResponseAdapter.adapt(res, type: :completion)
70
71
  .extend(Module.new { define_method(:__tools__) { tools } })
71
- finish_trace(operation: "chat", model: params[:model], res:, span:)
72
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
73
+ res
72
74
  end
73
75
 
74
76
  ##
@@ -181,10 +183,12 @@ module LLM
181
183
  end
182
184
 
183
185
  def headers
184
- (@headers || {}).merge(
185
- "Content-Type" => "application/json",
186
- "Authorization" => "Bearer #{@key}"
187
- )
186
+ lock do
187
+ (@headers || {}).merge(
188
+ "Content-Type" => "application/json",
189
+ "Authorization" => "Bearer #{@key}"
190
+ )
191
+ end
188
192
  end
189
193
 
190
194
  def stream_parser
@@ -48,8 +48,6 @@ module LLM
48
48
  def initialize(provider, options = {})
49
49
  super
50
50
  @exporter = options.delete(:exporter)
51
- @root_span = nil
52
- @root_context = nil
53
51
  setup!
54
52
  end
55
53
 
@@ -69,22 +67,23 @@ module LLM
69
67
  )
70
68
  attrs = attributes.compact
71
69
  attrs["llm.trace_group_id"] = trace_group_id.to_s
72
- @root_span = @tracer.start_span(
70
+ root_span = @tracer.start_span(
73
71
  name,
74
72
  kind: :server,
75
73
  attributes: attrs,
76
74
  with_parent: parent_ctx
77
75
  )
78
- @root_context = ::OpenTelemetry::Trace.context_with_span(@root_span)
76
+ thread[thread_root_span_key] = root_span
77
+ thread[thread_root_context_key] = ::OpenTelemetry::Trace.context_with_span(root_span)
79
78
  self
80
79
  end
81
80
 
82
81
  ##
83
82
  # @return [self]
84
83
  def stop_trace
85
- @root_span&.finish
86
- @root_span = nil
87
- @root_context = nil
84
+ thread[thread_root_span_key]&.finish
85
+ thread[thread_root_span_key] = nil
86
+ thread[thread_root_context_key] = nil
88
87
  self
89
88
  end
90
89
 
@@ -197,11 +196,30 @@ module LLM
197
196
  ##
198
197
  # @api private
199
198
  def create_span(name, kind: :client, attributes: {})
199
+ root_context = thread[thread_root_context_key]
200
200
  opts = {kind:, attributes:}
201
- opts[:with_parent] = @root_context if @root_context
201
+ opts[:with_parent] = root_context if root_context
202
202
  @tracer.start_span(name, **opts)
203
203
  end
204
204
 
205
+ ##
206
+ # @api private
207
+ def thread_root_span_key
208
+ @thread_root_span_key ||= :"llm.telemetry.root_span.#{object_id}"
209
+ end
210
+
211
+ ##
212
+ # @api private
213
+ def thread_root_context_key
214
+ @thread_root_context_key ||= :"llm.telemetry.root_context.#{object_id}"
215
+ end
216
+
217
+ ##
218
+ # @api private
219
+ def thread
220
+ Thread.current
221
+ end
222
+
205
223
  ##
206
224
  # Converts a string trace_group_id to an OpenTelemetry SpanContext so all
207
225
  # spans created with this context share the same trace_id.
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "4.4.0"
4
+ VERSION = "4.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.0
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antar Azri