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
@@ -35,13 +35,13 @@ class LLM::OpenAI
35
35
  req = LLM::Transport::Request.post(path("/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, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
38
+ res, span, tracer, request_id = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
39
39
  content_type = res["content-type"].to_s.split(";").first
40
40
  content_type = content_type.empty? ? LLM::Mime[".#{response_format}"] : content_type
41
41
  data = "data:#{content_type};base64,#{[io.string].pack("m0")}"
42
42
  res.body = LLM::Object.from(audio: data)
43
43
  res = ResponseAdapter.adapt(LLM::Response.new(res), type: :audio)
44
- tracer.on_request_finish(operation: "request", model:, res:, span:)
44
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
45
45
  res
46
46
  end
47
47
 
@@ -62,9 +62,9 @@ class LLM::OpenAI
62
62
  req = LLM::Transport::Request.post(path("/audio/transcriptions"), headers)
63
63
  req["content-type"] = multi.content_type
64
64
  transport.set_body_stream(req, multi.body)
65
- res, span, tracer = execute(request: req, operation: "request")
65
+ res, span, tracer, request_id = execute(request: req, operation: "request")
66
66
  res = LLM::Response.new(res)
67
- tracer.on_request_finish(operation: "request", model:, res:, span:)
67
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
68
68
  res
69
69
  end
70
70
 
@@ -86,9 +86,9 @@ class LLM::OpenAI
86
86
  req = LLM::Transport::Request.post(path("/audio/translations"), headers)
87
87
  req["content-type"] = multi.content_type
88
88
  transport.set_body_stream(req, multi.body)
89
- res, span, tracer = execute(request: req, operation: "request")
89
+ res, span, tracer, request_id = execute(request: req, operation: "request")
90
90
  res = LLM::Response.new(res)
91
- tracer.on_request_finish(operation: "request", model:, res:, span:)
91
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
92
92
  res
93
93
  end
94
94
 
@@ -21,10 +21,13 @@ class LLM::OpenAI
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::OpenAI::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::OpenAI
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::OpenAI
41
41
  def all(**params)
42
42
  query = URI.encode_www_form(params)
43
43
  req = LLM::Transport::Request.get(path("/files?#{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: :enumerable)
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
 
@@ -63,9 +63,9 @@ class LLM::OpenAI
63
63
  req = LLM::Transport::Request.post(path("/files"), headers)
64
64
  req["content-type"] = multi.content_type
65
65
  transport.set_body_stream(req, multi.body)
66
- res, span, tracer = execute(request: req, operation: "request")
66
+ res, span, tracer, request_id = execute(request: req, operation: "request")
67
67
  res = ResponseAdapter.adapt(res, type: :file)
68
- tracer.on_request_finish(operation: "request", res:, span:)
68
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
69
69
  res
70
70
  end
71
71
 
@@ -84,9 +84,9 @@ class LLM::OpenAI
84
84
  file_id = file.respond_to?(:id) ? file.id : file
85
85
  query = URI.encode_www_form(params)
86
86
  req = LLM::Transport::Request.get(path("/files/#{file_id}?#{query}"), headers)
87
- res, span, tracer = execute(request: req, operation: "request")
87
+ res, span, tracer, request_id = execute(request: req, operation: "request")
88
88
  res = ResponseAdapter.adapt(res, type: :file)
89
- tracer.on_request_finish(operation: "request", res:, span:)
89
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
90
90
  res
91
91
  end
92
92
 
@@ -107,9 +107,9 @@ class LLM::OpenAI
107
107
  file_id = file.respond_to?(:id) ? file.id : file
108
108
  req = LLM::Transport::Request.get(path("/files/#{file_id}/content?#{query}"), headers)
109
109
  io = StringIO.new("".b)
110
- res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
110
+ res, span, tracer, request_id = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
111
111
  res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
112
- tracer.on_request_finish(operation: "request", res:, span:)
112
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
113
113
  res
114
114
  end
115
115
 
@@ -126,9 +126,9 @@ class LLM::OpenAI
126
126
  def delete(file:)
127
127
  file_id = file.respond_to?(:id) ? file.id : file
128
128
  req = LLM::Transport::Request.delete(path("/files/#{file_id}"), headers)
129
- res, span, tracer = execute(request: req, operation: "request")
129
+ res, span, tracer, request_id = execute(request: req, operation: "request")
130
130
  res = LLM::Response.new(res)
131
- tracer.on_request_finish(operation: "request", res:, span:)
131
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
132
132
  res
133
133
  end
134
134
 
@@ -38,9 +38,9 @@ class LLM::OpenAI
38
38
  def create(prompt:, model: "gpt-image-1-mini", output_format: "png", **params)
39
39
  req = LLM::Transport::Request.post(path("/images/generations"), headers)
40
40
  req.body = LLM.json.dump({prompt:, n: 1, model:, output_format:}.merge!(params))
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: :image)
43
- tracer.on_request_finish(operation: "request", model:, res:, span:)
43
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
44
44
  res
45
45
  end
46
46
 
@@ -64,9 +64,9 @@ class LLM::OpenAI
64
64
  req = LLM::Transport::Request.post(path("/images/edits"), headers)
65
65
  req["content-type"] = multi.content_type
66
66
  transport.set_body_stream(req, multi.body)
67
- res, span, tracer = execute(request: req, operation: "request")
67
+ res, span, tracer, request_id = execute(request: req, operation: "request")
68
68
  res = ResponseAdapter.adapt(res, type: :image)
69
- tracer.on_request_finish(operation: "request", model:, res:, span:)
69
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
70
70
  res
71
71
  end
72
72
 
@@ -40,9 +40,9 @@ class LLM::OpenAI
40
40
  def all(**params)
41
41
  query = URI.encode_www_form(params)
42
42
  req = LLM::Transport::Request.get(path("/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
 
@@ -50,9 +50,9 @@ class LLM::OpenAI
50
50
  req = LLM::Transport::Request.post(path("/moderations"), headers)
51
51
  input = RequestAdapter::Moderation.new(input).adapt
52
52
  req.body = LLM.json.dump({input:, model:}.merge!(params))
53
- res, span, tracer = execute(request: req, operation: "request")
53
+ res, span, tracer, request_id = execute(request: req, operation: "request")
54
54
  res = ResponseAdapter.adapt(res, type: :moderations)
55
- tracer.on_request_finish(operation: "request", model:, res:, span:)
55
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
56
56
  res
57
57
  end
58
58
 
@@ -36,7 +36,7 @@ class LLM::OpenAI
36
36
  {
37
37
  response_format: {
38
38
  type: "json_schema",
39
- json_schema: {name: "JSONSchema", schema:}
39
+ json_schema: {name: "JSONSchema", schema: LLM::Schema::Utils.close(schema)}
40
40
  }
41
41
  }
42
42
  end
@@ -54,10 +54,10 @@ class LLM::OpenAI
54
54
  params.delete(:input)
55
55
  body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
56
56
  transport.set_body_stream(req, StringIO.new(body))
57
- res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
57
+ res, span, tracer, request_id = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
58
58
  res = ResponseAdapter.adapt(res, type: :responds)
59
59
  .extend(Module.new { define_method(:__tools__) { tools } })
60
- tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
60
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
61
61
  res
62
62
  end
63
63
 
@@ -71,9 +71,9 @@ class LLM::OpenAI
71
71
  response_id = response.respond_to?(:id) ? response.id : response
72
72
  query = URI.encode_www_form(params)
73
73
  req = LLM::Transport::Request.get(path("/responses/#{response_id}?#{query}"), headers)
74
- res, span, tracer = execute(request: req, operation: "request")
74
+ res, span, tracer, request_id = execute(request: req, operation: "request")
75
75
  res = ResponseAdapter.adapt(res, type: :responds)
76
- tracer.on_request_finish(operation: "request", res:, span:)
76
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
77
77
  res
78
78
  end
79
79
 
@@ -86,9 +86,9 @@ class LLM::OpenAI
86
86
  def delete(response)
87
87
  response_id = response.respond_to?(:id) ? response.id : response
88
88
  req = LLM::Transport::Request.delete(path("/responses/#{response_id}"), headers)
89
- res, span, tracer = execute(request: req, operation: "request")
89
+ res, span, tracer, request_id = execute(request: req, operation: "request")
90
90
  res = LLM::Response.new(res)
91
- tracer.on_request_finish(operation: "request", res:, span:)
91
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
92
92
  res
93
93
  end
94
94
 
@@ -98,7 +98,7 @@ class LLM::OpenAI
98
98
  def adapt_function(fn)
99
99
  {
100
100
  type: "function", name: fn.name, description: fn.description,
101
- parameters: fn.params.to_h.merge(additionalProperties: false), strict: false
101
+ parameters: LLM::Schema::Utils.close(fn.params), strict: false
102
102
  }.compact
103
103
  end
104
104
 
@@ -112,7 +112,7 @@ class LLM::OpenAI
112
112
  return {} unless params && params[:schema]
113
113
  schema = params.delete(:schema)
114
114
  schema = schema.respond_to?(:object) ? schema.object : schema
115
- schema = schema.to_h.merge(additionalProperties: false)
115
+ schema = LLM::Schema::Utils.close(schema)
116
116
  name = "JSONSchema"
117
117
  {text: {format: {type: "json_schema", name:, schema:}}}
118
118
  end
@@ -32,9 +32,9 @@ class LLM::OpenAI
32
32
  def all(**params)
33
33
  query = URI.encode_www_form(params)
34
34
  req = LLM::Transport::Request.get(path("/vector_stores?#{query}"), headers)
35
- res, span, tracer = execute(request: req, operation: "request")
35
+ res, span, tracer, request_id = execute(request: req, operation: "request")
36
36
  res = ResponseAdapter.adapt(res, type: :enumerable)
37
- tracer.on_request_finish(operation: "request", res:, span:)
37
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
38
38
  res
39
39
  end
40
40
 
@@ -49,9 +49,9 @@ class LLM::OpenAI
49
49
  def create(name:, file_ids: nil, **params)
50
50
  req = LLM::Transport::Request.post(path("/vector_stores"), headers)
51
51
  req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
52
- res, span, tracer = execute(request: req, operation: "request")
52
+ res, span, tracer, request_id = execute(request: req, operation: "request")
53
53
  res = LLM::Response.new(res)
54
- tracer.on_request_finish(operation: "request", res:, span:)
54
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
55
55
  res
56
56
  end
57
57
 
@@ -73,9 +73,9 @@ class LLM::OpenAI
73
73
  def get(vector:)
74
74
  vector_id = vector.respond_to?(:id) ? vector.id : vector
75
75
  req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}"), headers)
76
- res, span, tracer = execute(request: req, operation: "request")
76
+ res, span, tracer, request_id = execute(request: req, operation: "request")
77
77
  res = LLM::Response.new(res)
78
- tracer.on_request_finish(operation: "request", res:, span:)
78
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
79
79
  res
80
80
  end
81
81
 
@@ -91,9 +91,9 @@ class LLM::OpenAI
91
91
  vector_id = vector.respond_to?(:id) ? vector.id : vector
92
92
  req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}"), headers)
93
93
  req.body = LLM.json.dump(params.merge({name:}).compact)
94
- res, span, tracer = execute(request: req, operation: "request")
94
+ res, span, tracer, request_id = execute(request: req, operation: "request")
95
95
  res = LLM::Response.new(res)
96
- tracer.on_request_finish(operation: "request", res:, span:)
96
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
97
97
  res
98
98
  end
99
99
 
@@ -106,9 +106,9 @@ class LLM::OpenAI
106
106
  def delete(vector:)
107
107
  vector_id = vector.respond_to?(:id) ? vector.id : vector
108
108
  req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}"), headers)
109
- res, span, tracer = execute(request: req, operation: "request")
109
+ res, span, tracer, request_id = execute(request: req, operation: "request")
110
110
  res = LLM::Response.new(res)
111
- tracer.on_request_finish(operation: "request", res:, span:)
111
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
112
112
  res
113
113
  end
114
114
 
@@ -124,9 +124,9 @@ class LLM::OpenAI
124
124
  vector_id = vector.respond_to?(:id) ? vector.id : vector
125
125
  req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/search"), headers)
126
126
  req.body = LLM.json.dump(params.merge({query:}).compact)
127
- res, span, tracer = execute(request: req, operation: "retrieval")
127
+ res, span, tracer, request_id = execute(request: req, operation: "retrieval")
128
128
  res = ResponseAdapter.adapt(res, type: :enumerable)
129
- tracer.on_request_finish(operation: "retrieval", res:, span:)
129
+ tracer.on_request_finish(operation: "retrieval", res:, span:, request_id:)
130
130
  res
131
131
  end
132
132
 
@@ -141,9 +141,9 @@ class LLM::OpenAI
141
141
  vector_id = vector.respond_to?(:id) ? vector.id : vector
142
142
  query = URI.encode_www_form(params)
143
143
  req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files?#{query}"), headers)
144
- res, span, tracer = execute(request: req, operation: "request")
144
+ res, span, tracer, request_id = execute(request: req, operation: "request")
145
145
  res = ResponseAdapter.adapt(res, type: :enumerable)
146
- tracer.on_request_finish(operation: "request", res:, span:)
146
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
147
147
  res
148
148
  end
149
149
 
@@ -161,9 +161,9 @@ class LLM::OpenAI
161
161
  file_id = file.respond_to?(:id) ? file.id : file
162
162
  req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files"), headers)
163
163
  req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
164
- res, span, tracer = execute(request: req, operation: "request")
164
+ res, span, tracer, request_id = execute(request: req, operation: "request")
165
165
  res = LLM::Response.new(res)
166
- tracer.on_request_finish(operation: "request", res:, span:)
166
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
167
167
  res
168
168
  end
169
169
  alias_method :create_file, :add_file
@@ -192,9 +192,9 @@ class LLM::OpenAI
192
192
  file_id = file.respond_to?(:id) ? file.id : file
193
193
  req = LLM::Transport::Request.post(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
194
194
  req.body = LLM.json.dump(params.merge({attributes:}).compact)
195
- res, span, tracer = execute(request: req, operation: "request")
195
+ res, span, tracer, request_id = execute(request: req, operation: "request")
196
196
  res = LLM::Response.new(res)
197
- tracer.on_request_finish(operation: "request", res:, span:)
197
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
198
198
  res
199
199
  end
200
200
 
@@ -210,9 +210,9 @@ class LLM::OpenAI
210
210
  file_id = file.respond_to?(:id) ? file.id : file
211
211
  query = URI.encode_www_form(params)
212
212
  req = LLM::Transport::Request.get(path("/vector_stores/#{vector_id}/files/#{file_id}?#{query}"), headers)
213
- res, span, tracer = execute(request: req, operation: "request")
213
+ res, span, tracer, request_id = execute(request: req, operation: "request")
214
214
  res = LLM::Response.new(res)
215
- tracer.on_request_finish(operation: "request", res:, span:)
215
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
216
216
  res
217
217
  end
218
218
 
@@ -227,9 +227,9 @@ class LLM::OpenAI
227
227
  vector_id = vector.respond_to?(:id) ? vector.id : vector
228
228
  file_id = file.respond_to?(:id) ? file.id : file
229
229
  req = LLM::Transport::Request.delete(path("/vector_stores/#{vector_id}/files/#{file_id}"), headers)
230
- res, span, tracer = execute(request: req, operation: "request")
230
+ res, span, tracer, request_id = execute(request: req, operation: "request")
231
231
  res = LLM::Response.new(res)
232
- tracer.on_request_finish(operation: "request", res:, span:)
232
+ tracer.on_request_finish(operation: "request", res:, span:, request_id:)
233
233
  res
234
234
  end
235
235
 
@@ -54,9 +54,9 @@ module LLM
54
54
  def embed(input, model: "text-embedding-3-small", **params)
55
55
  req = LLM::Transport::Request.post(path("/embeddings"), headers)
56
56
  req.body = LLM.json.dump({input:, model:}.merge!(params))
57
- res, span, tracer = execute(request: req, operation: "embeddings", model:)
57
+ res, span, tracer, request_id = execute(request: req, operation: "embeddings", model:)
58
58
  res = ResponseAdapter.adapt(res, type: :embedding)
59
- tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
59
+ tracer.on_request_finish(operation: "embeddings", model:, res:, span:, request_id:)
60
60
  res
61
61
  end
62
62
 
@@ -74,10 +74,10 @@ module LLM
74
74
  params, stream, tools, role = normalize_complete_params(params)
75
75
  req, messages = build_complete_request(prompt, params, role)
76
76
  tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
77
- res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
77
+ res, span, tracer, request_id = execute(request: req, stream: stream, operation: "chat", model: params[:model])
78
78
  res = ResponseAdapter.adapt(res, type: :completion)
79
79
  .extend(Module.new { define_method(:__tools__) { tools } })
80
- tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
80
+ tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:, request_id:)
81
81
  res
82
82
  end
83
83
 
@@ -156,10 +156,10 @@ module LLM
156
156
  # @param [LLM::Function] fn
157
157
  # @return [Hash]
158
158
  def adapt_function(fn)
159
- params = fn.params.to_h
159
+ params = LLM::Schema::Utils.close(fn.params)
160
160
  {
161
- type: "function", name: fn.name,
162
- function: {name: fn.name, description: fn.description, parameters: params}
161
+ type: "function",
162
+ function: {name: fn.name, description: fn.description, parameters: params, strict: false}
163
163
  }.compact
164
164
  end
165
165
 
@@ -203,7 +203,9 @@ module LLM
203
203
 
204
204
  def headers
205
205
  lock do
206
- (@headers || {}).merge(
206
+ (@headers || {})
207
+ .merge(temporary_headers)
208
+ .merge(
207
209
  "Content-Type" => "application/json",
208
210
  "Authorization" => "Bearer #{@key}"
209
211
  )
@@ -34,9 +34,9 @@ class LLM::XAI
34
34
  def create(prompt:, model: "grok-imagine-image-quality", **params)
35
35
  req = LLM::Transport::Request.post(path("/images/generations"), headers)
36
36
  req.body = LLM.json.dump({prompt:, n: 1, model:, response_format: "b64_json"}.merge!(params))
37
- res, span, tracer = execute(request: req, operation: "request")
37
+ res, span, tracer, request_id = execute(request: req, operation: "request")
38
38
  res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
39
- tracer.on_request_finish(operation: "request", model:, res:, span:)
39
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
40
40
  res
41
41
  end
42
42
 
@@ -61,9 +61,9 @@ class LLM::XAI
61
61
  image: image_url(image),
62
62
  response_format: "b64_json"
63
63
  }.merge!(params))
64
- res, span, tracer = execute(request: req, operation: "request")
64
+ res, span, tracer, request_id = execute(request: req, operation: "request")
65
65
  res = LLM::OpenAI::ResponseAdapter.adapt(res, type: :image)
66
- tracer.on_request_finish(operation: "request", model:, res:, span:)
66
+ tracer.on_request_finish(operation: "request", model:, res:, span:, request_id:)
67
67
  res
68
68
  end
69
69
 
data/lib/llm/schema.rb CHANGED
@@ -81,6 +81,30 @@ class LLM::Schema
81
81
  def fetch(properties, name)
82
82
  properties[name] || properties.fetch(name.to_s)
83
83
  end
84
+
85
+ ##
86
+ # Returns a copy of a JSON schema where every object is
87
+ # closed with "additionalProperties": false. OpenAI and
88
+ # Azure reject tool parameters that leave an object open,
89
+ # and they validate nested objects as well as the root.
90
+ # @param [Object] node
91
+ # A schema, a Hash of properties, or an Array of either
92
+ # @return [Object]
93
+ def close(node)
94
+ case node
95
+ when LLM::Schema::Leaf
96
+ close(node.to_h)
97
+ when LLM::Object
98
+ close(node.to_h)
99
+ when Hash
100
+ node.to_h { |key, value| [key, close(value)] }
101
+ .then { _1[:type] == "object" ? _1.merge(additionalProperties: false) : _1 }
102
+ when Array
103
+ node.map { close(_1) }
104
+ else
105
+ node
106
+ end
107
+ end
84
108
  end
85
109
 
86
110
  ##
@@ -86,7 +86,7 @@ module LLM
86
86
 
87
87
  ##
88
88
  # @param (see LLM::Tracer#on_request_start)
89
- def on_request_start(operation:, model: nil, inputs: nil)
89
+ def on_request_start(operation:, model: nil, inputs: nil, request_id:)
90
90
  case operation
91
91
  when "chat" then start_chat(operation:, model:, inputs:)
92
92
  when "retrieval" then start_retrieval(operation:)
@@ -96,7 +96,7 @@ module LLM
96
96
 
97
97
  ##
98
98
  # @param (see LLM::Tracer#on_request_finish)
99
- def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
99
+ def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
100
100
  return nil unless span
101
101
  case operation
102
102
  when "chat" then finish_chat(operation:, model:, res:, span:, outputs:, metadata:)
@@ -107,7 +107,7 @@ module LLM
107
107
 
108
108
  ##
109
109
  # @param (see LLM::Tracer#on_request_error)
110
- def on_request_error(ex:, span:)
110
+ def on_request_error(ex:, span:, request_id:)
111
111
  return nil unless span
112
112
  attributes = {"error.type" => ex.class.to_s}.compact
113
113
  attributes.each { span.set_attribute(_1, _2) }
@@ -305,7 +305,7 @@ module LLM
305
305
  "gen_ai.operation.name" => operation,
306
306
  "gen_ai.request.model" => model,
307
307
  "gen_ai.response.id" => res.id,
308
- "gen_ai.response.model" => model,
308
+ "gen_ai.response.model" => res.model,
309
309
  "gen_ai.usage.input_tokens" => res.usage.input_tokens,
310
310
  "gen_ai.usage.output_tokens" => res.usage.output_tokens,
311
311
  "output.value" => serialize_request_value(output_value)
data/lib/llm/tracer.rb CHANGED
@@ -66,8 +66,12 @@ module LLM
66
66
  # @param [String] operation
67
67
  # @param [String] model
68
68
  # @param [Hash, nil] inputs Optional span attributes (e.g. gen_ai.input.messages) from llm.rb or caller.
69
+ # @param [String] request_id
70
+ # The id of the request. It is a UUIDv7, and it is shared by
71
+ # {#on_request_start}, {#on_request_finish}, and {#on_request_error}
72
+ # for the same request.
69
73
  # @return [void]
70
- def on_request_start(operation:, model: nil, inputs: nil)
74
+ def on_request_start(operation:, model: nil, inputs: nil, request_id:)
71
75
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
72
76
  end
73
77
 
@@ -79,8 +83,10 @@ module LLM
79
83
  # @param [String] model
80
84
  # @param [Hash, nil] outputs Optional span attributes (e.g. gen_ai.output.messages) from llm.rb or caller.
81
85
  # @param [Hash, nil] metadata Optional metadata from llm.rb or caller.
86
+ # @param [String] request_id
87
+ # The id of the request, as passed to {#on_request_start}
82
88
  # @return [void]
83
- def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil)
89
+ def on_request_finish(operation:, res:, model: nil, span: nil, outputs: nil, metadata: nil, request_id:)
84
90
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
85
91
  end
86
92
 
@@ -88,8 +94,10 @@ module LLM
88
94
  # Called when an LLM provider request fails.
89
95
  # @param [LLM::Error] ex
90
96
  # @param [Object, nil] span
97
+ # @param [String] request_id
98
+ # The id of the request, as passed to {#on_request_start}
91
99
  # @return [void]
92
- def on_request_error(ex:, span:)
100
+ def on_request_error(ex:, span:, request_id:)
93
101
  raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
94
102
  end
95
103
 
@@ -33,10 +33,11 @@ class LLM::Transport
33
33
  stream &&= LLM::Object.from(streamer: stream, parser: stream_parser, decoder: stream_decoder)
34
34
  owner = transport.request_owner
35
35
  tracer = self.tracer
36
- span = tracer.on_request_start(operation:, model:, inputs:)
36
+ request_id = SecureRandom.uuid_v7
37
+ span = tracer.on_request_start(operation:, model:, inputs:, request_id:)
37
38
  res = transport.request(request, owner:, stream:, &b)
38
39
  res = LLM::Transport::Response.from(res)
39
- [handle_response(res, tracer, span), span, tracer]
40
+ [handle_response(res, tracer, span, request_id), span, tracer, request_id]
40
41
  rescue *transport.interrupt_errors
41
42
  raise LLM::Interrupt, "request interrupted" if transport.interrupted?(owner)
42
43
  raise
@@ -48,9 +49,12 @@ class LLM::Transport
48
49
  # The response to handle
49
50
  # @param [Object, nil] span
50
51
  # The span
52
+ # @param [String] request_id
53
+ # The id of the request being handled
51
54
  # @return [LLM::Transport::Response]
52
- def handle_response(res, tracer, span)
53
- res.ok? ? res.body = parse_response(res) : error_handler.new(tracer, span, res).raise_error!
55
+ def handle_response(res, tracer, span, request_id)
56
+ res.ok? ? res.body = parse_response(res) :
57
+ error_handler.new(tracer, span, res, request_id).raise_error!
54
58
  res
55
59
  end
56
60
 
data/lib/llm/utils.rb CHANGED
@@ -48,6 +48,19 @@ module LLM
48
48
  (defined?(::Sequel::Model) and ::Sequel::Model === obj)
49
49
  end
50
50
 
51
+ ##
52
+ # Returns the UTC time encoded in a UUIDv7, or nil when the
53
+ # given value is not a UUIDv7. The first 48 bits of a UUIDv7
54
+ # are a Unix millisecond timestamp.
55
+ # @param [Object] id
56
+ # @return [Time, nil]
57
+ def timestamp(id)
58
+ hex = id.to_s.delete("-")
59
+ return nil unless hex.match?(/\A\h{32}\z/)
60
+ return nil unless hex[12] == "7"
61
+ Time.at(hex[0, 12].to_i(16) / 1000.0).utc
62
+ end
63
+
51
64
  ##
52
65
  # Normalizes an HTTP API base path.
53
66
  #
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "15.2.2"
4
+ VERSION = "15.4.0"
5
5
  end
data/llm.gemspec CHANGED
@@ -35,9 +35,9 @@ DESCRIPTION
35
35
  spec.executables = ["llm.rb"]
36
36
  spec.require_paths = ["lib"]
37
37
  spec.post_install_message = "\n" \
38
- "Got a question about llm.rb? " \
38
+ "Want to see what I'm building with llm.rb? " \
39
39
  "\n" \
40
- "The https://r.uby.dev website can help." \
40
+ "Checkout the https://r.uby.dev website." \
41
41
  "\n\n"
42
42
 
43
43
  spec.add_development_dependency "webmock", "~> 3.24.0"