llm.rb 15.2.2 → 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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +197 -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 +44 -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 +10 -3
  37. data/lib/llm/context.rb +62 -26
  38. data/lib/llm/guard.rb +2 -8
  39. data/lib/llm/message.rb +18 -7
  40. data/lib/llm/provider.rb +74 -16
  41. data/lib/llm/providers/alibaba.rb +15 -0
  42. data/lib/llm/providers/anthropic/error_handler.rb +5 -2
  43. data/lib/llm/providers/anthropic/files.rb +12 -12
  44. data/lib/llm/providers/anthropic/models.rb +2 -2
  45. data/lib/llm/providers/anthropic.rb +5 -3
  46. data/lib/llm/providers/bedrock/error_handler.rb +3 -2
  47. data/lib/llm/providers/bedrock/models.rb +5 -3
  48. data/lib/llm/providers/bedrock.rb +5 -3
  49. data/lib/llm/providers/deepinfra/audio.rb +4 -4
  50. data/lib/llm/providers/deepinfra/images.rb +4 -4
  51. data/lib/llm/providers/google/error_handler.rb +5 -2
  52. data/lib/llm/providers/google/files.rb +10 -10
  53. data/lib/llm/providers/google/images.rb +2 -2
  54. data/lib/llm/providers/google/models.rb +2 -2
  55. data/lib/llm/providers/google.rb +7 -7
  56. data/lib/llm/providers/mistral.rb +3 -1
  57. data/lib/llm/providers/ollama/error_handler.rb +5 -2
  58. data/lib/llm/providers/ollama/models.rb +2 -2
  59. data/lib/llm/providers/ollama.rb +7 -5
  60. data/lib/llm/providers/openai/audio.rb +6 -6
  61. data/lib/llm/providers/openai/error_handler.rb +5 -2
  62. data/lib/llm/providers/openai/files.rb +10 -10
  63. data/lib/llm/providers/openai/images.rb +4 -4
  64. data/lib/llm/providers/openai/models.rb +2 -2
  65. data/lib/llm/providers/openai/moderations.rb +2 -2
  66. data/lib/llm/providers/openai/request_adapter.rb +1 -1
  67. data/lib/llm/providers/openai/responses.rb +8 -8
  68. data/lib/llm/providers/openai/vector_stores.rb +22 -22
  69. data/lib/llm/providers/openai.rb +10 -8
  70. data/lib/llm/providers/xai/images.rb +4 -4
  71. data/lib/llm/schema.rb +24 -0
  72. data/lib/llm/tracer/telemetry.rb +4 -4
  73. data/lib/llm/tracer.rb +11 -3
  74. data/lib/llm/transport/execution.rb +8 -4
  75. data/lib/llm/utils.rb +13 -0
  76. data/lib/llm/version.rb +1 -1
  77. data/llm.gemspec +2 -2
  78. metadata +5 -5
  79. data/lib/llm/guard/loop.rb +0 -89
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]
@@ -263,11 +279,21 @@ class LLM::Provider
263
279
  end
264
280
 
265
281
  ##
266
- # Add one or more headers to all requests
267
- # @example
282
+ # Add one or more headers to all requests, or scope them to a block.
283
+ #
284
+ # Without a block the headers are merged into the provider's defaults
285
+ # permanently. With a block the headers apply only to the current fiber,
286
+ # for the duration of the block, then the previous headers are restored.
287
+ # This is useful for a header that varies per context, such as
288
+ # OpenRouter's `x-session-id`.
289
+ # @example Permanent
268
290
  # llm = LLM.openai(key: ENV["KEY"])
269
291
  # llm.with("OpenAI-Organization" => ENV["ORG"])
270
292
  # llm.with("OpenAI-Project" => ENV["PROJECT"])
293
+ # @example Scoped
294
+ # llm.with("x-session-id" => ctx.id) do
295
+ # llm.complete("hello")
296
+ # end
271
297
  # @param [Hash<String,String>] headers
272
298
  # One or more headers
273
299
  # @note
@@ -275,12 +301,26 @@ class LLM::Provider
275
301
  # provided via the `headers:` keyword argument,
276
302
  # or provided directly as a Hash without the
277
303
  # `headers:` key namespace.
304
+ # @yield
278
305
  # @return [LLM::Provider]
279
- # Returns self
280
- def with(**headers)
306
+ # Returns self without a block, the block's value with one
307
+ def with(**headers, &block)
281
308
  headers = headers.merge(headers.delete(:headers) || {})
282
- lock do
283
- tap { @headers.merge!(headers) }
309
+ if block
310
+ wm = weakmaps.header
311
+ previous = wm[self]
312
+ wm[self] = (previous || {}).merge(headers)
313
+ block.call
314
+ else
315
+ lock { tap { @headers.merge!(headers) } }
316
+ end
317
+ ensure
318
+ if block
319
+ if previous.nil?
320
+ wm.respond_to?(:delete) ? wm.delete(self) : wm[self] = nil
321
+ else
322
+ wm[self] = previous
323
+ end
284
324
  end
285
325
  end
286
326
 
@@ -349,7 +389,7 @@ class LLM::Provider
349
389
  # @return [LLM::Tracer]
350
390
  # Returns the current scoped tracer override or provider default tracer
351
391
  def tracer
352
- weakmap[self] || @tracer || LLM::Tracer::Null.new(self)
392
+ weakmaps.tracer[self] || @tracer || LLM::Tracer::Null.new(self)
353
393
  end
354
394
 
355
395
  ##
@@ -378,17 +418,18 @@ class LLM::Provider
378
418
  # @yield
379
419
  # @return [Object]
380
420
  def with_tracer(tracer)
381
- had_override = weakmap.key?(self)
382
- previous = weakmap[self]
383
- weakmap[self] = tracer || LLM::Tracer::Null.new(self)
421
+ wm = weakmaps.tracer
422
+ had_override = wm.key?(self)
423
+ previous = wm[self]
424
+ wm[self] = tracer || LLM::Tracer::Null.new(self)
384
425
  yield
385
426
  ensure
386
427
  if had_override
387
- weakmap[self] = previous
388
- elsif weakmap.respond_to?(:delete)
389
- weakmap.delete(self)
428
+ wm[self] = previous
429
+ elsif wm.respond_to?(:delete)
430
+ wm.delete(self)
390
431
  else
391
- weakmap[self] = nil
432
+ wm[self] = nil
392
433
  end
393
434
  end
394
435
 
@@ -504,8 +545,25 @@ class LLM::Provider
504
545
  end
505
546
 
506
547
  ##
548
+ # Returns the temporary headers set by {#with}
549
+ # for the current fiber.
507
550
  # @api private
508
- def weakmap
509
- thread[:"llm.provider.weakmap"] ||= ObjectSpace::WeakMap.new
551
+ # @return [Hash]
552
+ def temporary_headers
553
+ weakmaps.header[self] || {}
554
+ end
555
+
556
+ ##
557
+ # @return [LLM::Object]
558
+ def weakmaps
559
+ if thread["llm.#{name}.weakmaps"]
560
+ thread["llm.#{name}.weakmaps"]
561
+ else
562
+ weakmaps = LLM::Object.from(
563
+ tracer: ObjectSpace::WeakMap.new,
564
+ header: ObjectSpace::WeakMap.new
565
+ )
566
+ thread["llm.#{name}.weakmaps"] = weakmaps
567
+ end
510
568
  end
511
569
  end
@@ -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
 
@@ -141,7 +141,9 @@ module LLM
141
141
 
142
142
  def headers
143
143
  lock do
144
- (@headers || {}).merge(
144
+ (@headers || {})
145
+ .merge(temporary_headers)
146
+ .merge(
145
147
  "Content-Type" => "application/json",
146
148
  "x-api-key" => @key,
147
149
  "anthropic-version" => "2023-06-01",
@@ -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
 
@@ -183,7 +183,9 @@ module LLM
183
183
 
184
184
  def headers
185
185
  lock do
186
- (@headers || {}).merge("Content-Type" => "application/json")
186
+ (@headers || {})
187
+ .merge(temporary_headers)
188
+ .merge("Content-Type" => "application/json")
187
189
  end
188
190
  end
189
191
 
@@ -23,9 +23,9 @@ class LLM::DeepInfra
23
23
  path = path("/v1/inference/#{model}", base_path: false)
24
24
  req = LLM::Transport::Request.post(path, headers)
25
25
  req.body = JSON.dump(params.merge(text: input))
26
- res, span, tracer = execute(request: req, operation: "request")
26
+ res, span, tracer, request_id = execute(request: req, operation: "request")
27
27
  res = ResponseAdapter.adapt LLM::Response.new(res), type: :audio
28
- tracer.on_request_finish(operation: "request", model:, res:, span:)
28
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
29
29
  res
30
30
  end
31
31
 
@@ -45,9 +45,9 @@ class LLM::DeepInfra
45
45
  req = LLM::Transport::Request.post(path, headers)
46
46
  req["content-type"] = multi.content_type
47
47
  transport.set_body_stream(req, multi.body)
48
- res, span, tracer = execute(request: req, operation: "request")
48
+ res, span, tracer, request_id = execute(request: req, operation: "request")
49
49
  res = LLM::Response.new(res)
50
- tracer.on_request_finish(operation: "request", model:, res:, span:)
50
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
51
51
  res
52
52
  end
53
53
 
@@ -44,9 +44,9 @@ class LLM::DeepInfra
44
44
  req = LLM::Transport::Request.post(path("/images/generations"), headers)
45
45
  params = {prompt:, model:, size:, n:, response_format:, quality:, style:}.compact
46
46
  req.body = LLM.json.dump(params)
47
- res, span, tracer = execute(request: req, operation: "request")
47
+ res, span, tracer, request_id = execute(request: req, operation: "request")
48
48
  res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
49
- tracer.on_request_finish(operation: "request", model:, res:, span:)
49
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
50
50
  res
51
51
  end
52
52
 
@@ -75,9 +75,9 @@ class LLM::DeepInfra
75
75
  req = LLM::Transport::Request.post(path("/images/edits"), headers)
76
76
  req["content-type"] = multi.content_type
77
77
  transport.set_body_stream(req, multi.body)
78
- res, span, tracer = execute(request: req, operation: "request")
78
+ res, span, tracer, request_id = execute(request: req, operation: "request")
79
79
  res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
80
- tracer.on_request_finish(operation: "request", model:, res:, span:)
80
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
81
81
  res
82
82
  end
83
83
 
@@ -21,10 +21,13 @@ class LLM::Google
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::Google::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::Google
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
@@ -46,9 +46,9 @@ class LLM::Google
46
46
  def all(**params)
47
47
  query = URI.encode_www_form(params.merge!(key: key))
48
48
  req = LLM::Transport::Request.get("/v1beta/files?#{query}", headers)
49
- res, span, tracer = execute(request: req, operation: "request")
49
+ res, span, tracer, request_id = execute(request: req, operation: "request")
50
50
  res = ResponseAdapter.adapt(res, type: :files)
51
- tracer.on_request_finish(operation: "request", res:, span:)
51
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
52
52
  res
53
53
  end
54
54
 
@@ -70,9 +70,9 @@ class LLM::Google
70
70
  req["X-Goog-Upload-Command"] = "upload, finalize"
71
71
  file.with_io do |io|
72
72
  transport.set_body_stream(req, io)
73
- res, span, tracer = execute(request: req, operation: "request")
73
+ res, span, tracer, request_id = execute(request: req, operation: "request")
74
74
  res = ResponseAdapter.adapt(res, type: :file)
75
- tracer.on_request_finish(operation: "request", res:, span:)
75
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
76
76
  res
77
77
  end
78
78
  end
@@ -92,9 +92,9 @@ class LLM::Google
92
92
  file_id = file.respond_to?(:name) ? file.name : file.to_s
93
93
  query = URI.encode_www_form(params.merge!(key: key))
94
94
  req = LLM::Transport::Request.get("/v1beta/#{file_id}?#{query}", headers)
95
- res, span, tracer = execute(request: req, operation: "request")
95
+ res, span, tracer, request_id = execute(request: req, operation: "request")
96
96
  res = ResponseAdapter.adapt(res, type: :file)
97
- tracer.on_request_finish(operation: "request", res:, span:)
97
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
98
98
  res
99
99
  end
100
100
 
@@ -112,9 +112,9 @@ class LLM::Google
112
112
  file_id = file.respond_to?(:name) ? file.name : file.to_s
113
113
  query = URI.encode_www_form(params.merge!(key: key))
114
114
  req = LLM::Transport::Request.delete("/v1beta/#{file_id}?#{query}", headers)
115
- res, span, tracer = execute(request: req, operation: "request")
115
+ res, span, tracer, request_id = execute(request: req, operation: "request")
116
116
  res = LLM::Response.new(res)
117
- tracer.on_request_finish(operation: "request", res:, span:)
117
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
118
118
  res
119
119
  end
120
120
 
@@ -134,9 +134,9 @@ class LLM::Google
134
134
  req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
135
135
  req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
136
136
  req.body = LLM.json.dump({file: {display_name: File.basename(file.path)}})
137
- res, span, tracer = execute(request: req, operation: "request")
137
+ res, span, tracer, request_id = execute(request: req, operation: "request")
138
138
  trace_res = LLM::Response.new(res)
139
- tracer.on_request_finish(operation: "request", res: trace_res, span:)
139
+ tracer.on_request_finish(operation: "request", res: trace_res, span:, request_id:)
140
140
  res["x-goog-upload-url"]
141
141
  end
142
142
 
@@ -51,9 +51,9 @@ class LLM::Google
51
51
  instances: [{prompt:}]
52
52
  })
53
53
  req.body = body
54
- res, span, tracer = execute(request: req, operation: "request")
54
+ res, span, tracer, request_id = execute(request: req, operation: "request")
55
55
  res = ResponseAdapter.adapt(res, type: :image)
56
- tracer.on_request_finish(operation: "request", model:, res:, span:)
56
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
57
57
  res
58
58
  end
59
59
 
@@ -40,9 +40,9 @@ class LLM::Google
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params.merge!(key: key))
42
42
  req = LLM::Transport::Request.get("/v1beta/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
 
@@ -58,9 +58,9 @@ module LLM
58
58
  path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
59
59
  req = LLM::Transport::Request.post(path, headers)
60
60
  req.body = LLM.json.dump({content: {parts: [{text: input}]}})
61
- res, span, tracer = execute(request: req, operation: "embeddings", model:)
61
+ res, span, tracer, request_id = execute(request: req, operation: "embeddings", model:)
62
62
  res = ResponseAdapter.adapt(res, type: :embedding)
63
- tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
63
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:, request_id:)
64
64
  res
65
65
  end
66
66
 
@@ -77,10 +77,10 @@ module LLM
77
77
  def complete(prompt, params = {})
78
78
  params, stream, tools, role, model = normalize_complete_params(params)
79
79
  req = build_complete_request(prompt, params, role, model, stream)
80
- res, span, tracer = execute(request: req, stream: stream, operation: "chat", model:)
80
+ res, span, tracer, request_id = execute(request: req, stream: stream, operation: "chat", model:)
81
81
  res = ResponseAdapter.adapt(res, type: :completion)
82
82
  .extend(Module.new { define_method(:__tools__) { tools } })
83
- tracer.on_request_finish(operation: "chat", model:, res:, span:)
83
+ tracer.on_request_finish(operation: "chat", model:, res:, span:, request_id:)
84
84
  res
85
85
  end
86
86
 
@@ -186,9 +186,9 @@ module LLM
186
186
 
187
187
  def headers
188
188
  lock do
189
- (@headers || {}).merge(
190
- "Content-Type" => "application/json"
191
- )
189
+ (@headers || {})
190
+ .merge(temporary_headers)
191
+ .merge("Content-Type" => "application/json")
192
192
  end
193
193
  end
194
194
 
@@ -136,7 +136,9 @@ module LLM
136
136
  # @api private
137
137
  def headers
138
138
  lock do
139
- (@headers || {}).merge(
139
+ (@headers || {})
140
+ .merge(temporary_headers)
141
+ .merge(
140
142
  "Authorization" => "Bearer #{@key}",
141
143
  "Content-Type" => "application/json"
142
144
  )
@@ -21,10 +21,13 @@ class LLM::Ollama
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::Ollama::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::Ollama
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
@@ -41,9 +41,9 @@ class LLM::Ollama
41
41
  def all(**params)
42
42
  query = URI.encode_www_form(params)
43
43
  req = LLM::Transport::Request.get("/api/tags?#{query}", headers)
44
- res, span, tracer = execute(request: req, operation: "request")
44
+ res, span, tracer, request_id = execute(request: req, operation: "request")
45
45
  res = ResponseAdapter.adapt(res, type: :models)
46
- tracer.on_request_finish(operation: "request", res:, span:)
46
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
47
47
  res
48
48
  end
49
49
 
@@ -50,9 +50,9 @@ module LLM
50
50
  params = {model:}.merge!(params)
51
51
  req = LLM::Transport::Request.post("/v1/embeddings", headers)
52
52
  req.body = LLM.json.dump({input:}.merge!(params))
53
- res, span, tracer = execute(request: req, operation: "embeddings", model:)
53
+ res, span, tracer, request_id = execute(request: req, operation: "embeddings", model:)
54
54
  res = ResponseAdapter.adapt(res, type: :embedding)
55
- tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
55
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:, request_id:)
56
56
  res
57
57
  end
58
58
 
@@ -69,10 +69,10 @@ module LLM
69
69
  def complete(prompt, params = {})
70
70
  params, stream, tools, role = normalize_complete_params(params)
71
71
  req = build_complete_request(prompt, params, role)
72
- res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
72
+ res, span, tracer, request_id = execute(request: req, stream: stream, operation: "chat", model: params[:model])
73
73
  res = ResponseAdapter.adapt(res, type: :completion)
74
74
  .extend(Module.new { define_method(:__tools__) { tools } })
75
- tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
75
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
76
76
  res
77
77
  end
78
78
 
@@ -113,7 +113,9 @@ module LLM
113
113
 
114
114
  def headers
115
115
  lock do
116
- (@headers || {}).merge(
116
+ (@headers || {})
117
+ .merge(temporary_headers)
118
+ .merge(
117
119
  "Content-Type" => "application/json",
118
120
  "Authorization" => "Bearer #{@key}"
119
121
  )