llm.rb 4.0.0 → 4.2.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.md +226 -192
  4. data/lib/llm/agent.rb +226 -0
  5. data/lib/llm/bot.rb +57 -28
  6. data/lib/llm/error.rb +4 -0
  7. data/lib/llm/function/tracing.rb +19 -0
  8. data/lib/llm/function.rb +16 -3
  9. data/lib/llm/json_adapter.rb +1 -1
  10. data/lib/llm/message.rb +7 -0
  11. data/lib/llm/prompt.rb +85 -0
  12. data/lib/llm/provider.rb +74 -10
  13. data/lib/llm/providers/anthropic/error_handler.rb +27 -5
  14. data/lib/llm/providers/anthropic/files.rb +22 -16
  15. data/lib/llm/providers/anthropic/models.rb +4 -3
  16. data/lib/llm/providers/anthropic.rb +6 -5
  17. data/lib/llm/providers/deepseek.rb +3 -3
  18. data/lib/llm/providers/gemini/error_handler.rb +34 -12
  19. data/lib/llm/providers/gemini/files.rb +18 -13
  20. data/lib/llm/providers/gemini/images.rb +4 -3
  21. data/lib/llm/providers/gemini/models.rb +4 -3
  22. data/lib/llm/providers/gemini.rb +36 -13
  23. data/lib/llm/providers/llamacpp.rb +3 -3
  24. data/lib/llm/providers/ollama/error_handler.rb +28 -6
  25. data/lib/llm/providers/ollama/models.rb +4 -3
  26. data/lib/llm/providers/ollama.rb +9 -7
  27. data/lib/llm/providers/openai/audio.rb +10 -7
  28. data/lib/llm/providers/openai/error_handler.rb +41 -14
  29. data/lib/llm/providers/openai/files.rb +19 -14
  30. data/lib/llm/providers/openai/images.rb +10 -7
  31. data/lib/llm/providers/openai/models.rb +4 -3
  32. data/lib/llm/providers/openai/moderations.rb +4 -3
  33. data/lib/llm/providers/openai/responses.rb +10 -7
  34. data/lib/llm/providers/openai/vector_stores.rb +34 -23
  35. data/lib/llm/providers/openai.rb +9 -7
  36. data/lib/llm/providers/xai.rb +3 -3
  37. data/lib/llm/providers/zai.rb +2 -2
  38. data/lib/llm/schema/object.rb +2 -2
  39. data/lib/llm/schema.rb +16 -2
  40. data/lib/llm/server_tool.rb +3 -3
  41. data/lib/llm/session.rb +3 -0
  42. data/lib/llm/tracer/logger.rb +192 -0
  43. data/lib/llm/tracer/null.rb +49 -0
  44. data/lib/llm/tracer/telemetry.rb +255 -0
  45. data/lib/llm/tracer.rb +134 -0
  46. data/lib/llm/version.rb +1 -1
  47. data/lib/llm.rb +5 -3
  48. data/llm.gemspec +4 -1
  49. metadata +39 -3
  50. data/lib/llm/builder.rb +0 -61
data/lib/llm/provider.rb CHANGED
@@ -37,6 +37,7 @@ class LLM::Provider
37
37
  @timeout = timeout
38
38
  @ssl = ssl
39
39
  @client = persistent ? persistent_client : transient_client
40
+ @tracer = LLM::Tracer::Null.new(self)
40
41
  @base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
41
42
  end
42
43
 
@@ -45,7 +46,7 @@ class LLM::Provider
45
46
  # @return [String]
46
47
  # @note The secret key is redacted in inspect for security reasons
47
48
  def inspect
48
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect}>"
49
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect} @tracer=#{@tracer.inspect}>"
49
50
  end
50
51
 
51
52
  ##
@@ -69,7 +70,7 @@ class LLM::Provider
69
70
  # llm = LLM.openai(key: ENV["KEY"])
70
71
  # messages = [{role: "system", content: "Your task is to answer all of my questions"}]
71
72
  # res = llm.complete("5 + 2 ?", messages:)
72
- # print "[#{res.choices[0].role}]", res.choices[0].content, "\n"
73
+ # print "[#{res.messages[0].role}]", res.messages[0].content, "\n"
73
74
  # @param [String] prompt
74
75
  # The input prompt to be completed
75
76
  # @param [Hash] params
@@ -91,10 +92,10 @@ class LLM::Provider
91
92
  # Starts a new chat powered by the chat completions API
92
93
  # @param prompt (see LLM::Provider#complete)
93
94
  # @param params (see LLM::Provider#complete)
94
- # @return [LLM::Bot]
95
+ # @return [LLM::Session]
95
96
  def chat(prompt, params = {})
96
97
  role = params.delete(:role)
97
- LLM::Bot.new(self, params).chat(prompt, role:)
98
+ LLM::Session.new(self, params).talk(prompt, role:)
98
99
  end
99
100
 
100
101
  ##
@@ -102,10 +103,10 @@ class LLM::Provider
102
103
  # @param prompt (see LLM::Provider#complete)
103
104
  # @param params (see LLM::Provider#complete)
104
105
  # @raise (see LLM::Provider#complete)
105
- # @return [LLM::Bot]
106
+ # @return [LLM::Session]
106
107
  def respond(prompt, params = {})
107
108
  role = params.delete(:role)
108
- LLM::Bot.new(self, params).respond(prompt, role:)
109
+ LLM::Session.new(self, params).respond(prompt, role:)
109
110
  end
110
111
 
111
112
  ##
@@ -234,6 +235,48 @@ class LLM::Provider
234
235
  raise NotImplementedError
235
236
  end
236
237
 
238
+ ##
239
+ # @return [Symbol]
240
+ def user_role
241
+ :user
242
+ end
243
+
244
+ ##
245
+ # @return [Symbol]
246
+ def system_role
247
+ :system
248
+ end
249
+
250
+ ##
251
+ # @return [Symbol]
252
+ def developer_role
253
+ :developer
254
+ end
255
+
256
+ ##
257
+ # @return [LLM::Tracer]
258
+ # Returns an LLM tracer
259
+ def tracer
260
+ @tracer
261
+ end
262
+
263
+ ##
264
+ # Set the tracer
265
+ # @example
266
+ # llm = LLM.openai(key: ENV["KEY"])
267
+ # llm.tracer = LLM::Tracer::Logger.new(llm, path: "/path/to/log.txt")
268
+ # # ...
269
+ # @param [LLM::Tracer] tracer
270
+ # A tracer
271
+ # @return [void]
272
+ def tracer=(tracer)
273
+ @tracer = if tracer.nil?
274
+ LLM::Tracer::Null.new(self)
275
+ else
276
+ tracer
277
+ end
278
+ end
279
+
237
280
  private
238
281
 
239
282
  attr_reader :client, :base_uri, :host, :port, :timeout, :ssl
@@ -285,7 +328,8 @@ class LLM::Provider
285
328
  # @raise [SystemCallError]
286
329
  # When there is a network error at the operating system level
287
330
  # @return [Net::HTTPResponse]
288
- def execute(request:, stream: nil, stream_parser: self.stream_parser, &b)
331
+ def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
332
+ span = @tracer.on_request_start(operation:, model:)
289
333
  args = (Net::HTTP === client) ? [request] : [URI.join(base_uri, request.path), request]
290
334
  res = if stream
291
335
  client.request(*args) do |res|
@@ -305,18 +349,20 @@ class LLM::Provider
305
349
  b ? client.request(*args) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 } :
306
350
  client.request(*args)
307
351
  end
308
- handle_response(res)
352
+ [handle_response(res, span), span]
309
353
  end
310
354
 
311
355
  ##
312
356
  # Handles the response from a request
313
357
  # @param [Net::HTTPResponse] res
314
358
  # The response to handle
359
+ # @param [Object, nil] span
360
+ # The span
315
361
  # @return [Net::HTTPResponse]
316
- def handle_response(res)
362
+ def handle_response(res, span)
317
363
  case res
318
364
  when Net::HTTPOK then res.body = parse_response(res)
319
- else error_handler.new(res).raise_error!
365
+ else error_handler.new(@tracer, span, res).raise_error!
320
366
  end
321
367
  res
322
368
  end
@@ -357,4 +403,22 @@ class LLM::Provider
357
403
  end
358
404
  end
359
405
  end
406
+
407
+ ##
408
+ # @return [Hash<Symbol, LLM::Tracer>]
409
+ def tracers
410
+ self.class.tracers
411
+ end
412
+
413
+ ##
414
+ # Finalizes tracing after a response has been adapted/wrapped.
415
+ # @param [String] operation
416
+ # @param [String, nil] model
417
+ # @param [LLM::Response] res
418
+ # @param [Object, nil] span
419
+ # @return [LLM::Response]
420
+ def finish_trace(operation:, res:, model: nil, span: nil)
421
+ @tracer.on_request_finish(operation:, model:, res:, span:)
422
+ res
423
+ end
360
424
  end
@@ -10,10 +10,21 @@ class LLM::Anthropic
10
10
  attr_reader :res
11
11
 
12
12
  ##
13
+ # @return [Object, nil]
14
+ # The span
15
+ attr_reader :span
16
+
17
+ ##
18
+ # @param [LLM::Tracer] tracer
19
+ # The tracer
20
+ # @param [Object, nil] span
21
+ # The span
13
22
  # @param [Net::HTTPResponse] res
14
23
  # The response from the server
15
24
  # @return [LLM::Anthropic::ErrorHandler]
16
- def initialize(res)
25
+ def initialize(tracer, span, res)
26
+ @tracer = tracer
27
+ @span = span
17
28
  @res = res
18
29
  end
19
30
 
@@ -21,15 +32,26 @@ class LLM::Anthropic
21
32
  # @raise [LLM::Error]
22
33
  # Raises a subclass of {LLM::Error LLM::Error}
23
34
  def raise_error!
35
+ ex = error
36
+ @tracer.on_request_error(ex:, span:)
37
+ ensure
38
+ raise(ex)
39
+ end
40
+
41
+ private
42
+
43
+ ##
44
+ # @return [LLM::Error]
45
+ def error
24
46
  case res
25
47
  when Net::HTTPServerError
26
- raise LLM::ServerError.new { _1.response = res }, "Server error"
48
+ LLM::ServerError.new("Server error").tap { _1.response = res }
27
49
  when Net::HTTPUnauthorized
28
- raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
50
+ LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
29
51
  when Net::HTTPTooManyRequests
30
- raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
52
+ LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
31
53
  else
32
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
54
+ LLM::Error.new("Unexpected response").tap { _1.response = res }
33
55
  end
34
56
  end
35
57
  end
@@ -10,10 +10,10 @@ class LLM::Anthropic
10
10
  # require "llm"
11
11
  #
12
12
  # llm = LLM.anthropic(key: ENV["KEY"])
13
- # bot = LLM::Bot.new(llm)
13
+ # ses = LLM::Session.new(llm)
14
14
  # file = llm.files.create file: "/books/goodread.pdf"
15
- # bot.chat ["Tell me about this PDF", file]
16
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
15
+ # ses.talk ["Tell me about this PDF", file]
16
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
17
17
  class Files
18
18
  ##
19
19
  # Returns a new Files object
@@ -38,8 +38,9 @@ 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 = execute(request: req)
42
- ResponseAdapter.adapt(res, type: :enumerable)
41
+ res, span = execute(request: req, operation: "request")
42
+ res = ResponseAdapter.adapt(res, type: :enumerable)
43
+ finish_trace(operation: "request", res:, span:)
43
44
  end
44
45
 
45
46
  ##
@@ -57,8 +58,9 @@ class LLM::Anthropic
57
58
  req = Net::HTTP::Post.new("/v1/files", headers)
58
59
  req["content-type"] = multi.content_type
59
60
  set_body_stream(req, multi.body)
60
- res = execute(request: req)
61
- ResponseAdapter.adapt(res, type: :file)
61
+ res, span = execute(request: req, operation: "request")
62
+ res = ResponseAdapter.adapt(res, type: :file)
63
+ finish_trace(operation: "request", res:, span:)
62
64
  end
63
65
 
64
66
  ##
@@ -76,8 +78,9 @@ class LLM::Anthropic
76
78
  file_id = file.respond_to?(:id) ? file.id : file
77
79
  query = URI.encode_www_form(params)
78
80
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
79
- res = execute(request: req)
80
- ResponseAdapter.adapt(res, type: :file)
81
+ res, span = execute(request: req, operation: "request")
82
+ res = ResponseAdapter.adapt(res, type: :file)
83
+ finish_trace(operation: "request", res:, span:)
81
84
  end
82
85
 
83
86
  ##
@@ -95,8 +98,9 @@ class LLM::Anthropic
95
98
  query = URI.encode_www_form(params)
96
99
  file_id = file.respond_to?(:id) ? file.id : file
97
100
  req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
98
- res = execute(request: req)
99
- ResponseAdapter.adapt(res, type: :file)
101
+ res, span = execute(request: req, operation: "request")
102
+ res = ResponseAdapter.adapt(res, type: :file)
103
+ finish_trace(operation: "request", res:, span:)
100
104
  end
101
105
  alias_method :retrieve_metadata, :get_metadata
102
106
 
@@ -113,8 +117,9 @@ class LLM::Anthropic
113
117
  def delete(file:)
114
118
  file_id = file.respond_to?(:id) ? file.id : file
115
119
  req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
116
- res = execute(request: req)
117
- LLM::Response.new(res)
120
+ res, span = execute(request: req, operation: "request")
121
+ res = LLM::Response.new(res)
122
+ finish_trace(operation: "request", res:, span:)
118
123
  end
119
124
 
120
125
  ##
@@ -137,8 +142,9 @@ class LLM::Anthropic
137
142
  file_id = file.respond_to?(:id) ? file.id : file
138
143
  req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
139
144
  io = StringIO.new("".b)
140
- res = execute(request: req) { |res| res.read_body { |chunk| io << chunk } }
141
- LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
145
+ res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
146
+ res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
147
+ finish_trace(operation: "request", res:, span:)
142
148
  end
143
149
 
144
150
  private
@@ -147,7 +153,7 @@ class LLM::Anthropic
147
153
  @provider.instance_variable_get(:@key)
148
154
  end
149
155
 
150
- [:headers, :execute, :set_body_stream].each do |m|
156
+ [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
151
157
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
152
158
  end
153
159
  end
@@ -40,13 +40,14 @@ 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 = execute(request: req)
44
- ResponseAdapter.adapt(res, type: :enumerable)
43
+ res, span = execute(request: req, operation: "request")
44
+ res = ResponseAdapter.adapt(res, type: :enumerable)
45
+ finish_trace(operation: "request", res:, span:)
45
46
  end
46
47
 
47
48
  private
48
49
 
49
- [:headers, :execute].each do |m|
50
+ [:headers, :execute, :finish_trace].each do |m|
50
51
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
51
52
  end
52
53
  end
@@ -10,9 +10,9 @@ module LLM
10
10
  # require "llm"
11
11
  #
12
12
  # llm = LLM.anthropic(key: ENV["KEY"])
13
- # bot = LLM::Bot.new(llm)
14
- # bot.chat ["Tell me about this photo", File.open("/images/dog.jpg", "rb")]
15
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
13
+ # ses = LLM::Session.new(llm)
14
+ # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
15
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
16
16
  class Anthropic < Provider
17
17
  require_relative "anthropic/error_handler"
18
18
  require_relative "anthropic/request_adapter"
@@ -43,9 +43,10 @@ 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 = execute(request: req, stream: stream)
47
- ResponseAdapter.adapt(res, type: :completion)
46
+ res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
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
50
  end
50
51
 
51
52
  ##
@@ -14,9 +14,9 @@ module LLM
14
14
  # require "llm"
15
15
  #
16
16
  # llm = LLM.deepseek(key: ENV["KEY"])
17
- # bot = LLM::Bot.new(llm)
18
- # bot.chat ["Tell me about this photo", File.open("/images/cat.jpg", "rb")]
19
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
17
+ # ses = LLM::Session.new(llm)
18
+ # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
19
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class DeepSeek < OpenAI
21
21
  require_relative "deepseek/request_adapter"
22
22
  include DeepSeek::RequestAdapter
@@ -10,10 +10,21 @@ class LLM::Gemini
10
10
  attr_reader :res
11
11
 
12
12
  ##
13
+ # @return [Object, nil]
14
+ # The span
15
+ attr_reader :span
16
+
17
+ ##
18
+ # @param [LLM::Tracer] tracer
19
+ # The tracer
20
+ # @param [Object, nil] span
21
+ # The span
13
22
  # @param [Net::HTTPResponse] res
14
23
  # The response from the server
15
24
  # @return [LLM::Gemini::ErrorHandler]
16
- def initialize(res)
25
+ def initialize(tracer, span, res)
26
+ @tracer = tracer
27
+ @span = span
17
28
  @res = res
18
29
  end
19
30
 
@@ -21,27 +32,38 @@ class LLM::Gemini
21
32
  # @raise [LLM::Error]
22
33
  # Raises a subclass of {LLM::Error LLM::Error}
23
34
  def raise_error!
35
+ ex = error
36
+ @tracer.on_request_error(ex:, span:)
37
+ ensure
38
+ raise(ex)
39
+ end
40
+
41
+ private
42
+
43
+ ##
44
+ # @return [LLM::Object]
45
+ def body
46
+ @body ||= LLM.json.load(res.body)
47
+ end
48
+
49
+ ##
50
+ # @return [LLM::Error]
51
+ def error
24
52
  case res
25
53
  when Net::HTTPServerError
26
- raise LLM::ServerError.new { _1.response = res }, "Server error"
54
+ LLM::ServerError.new("Server error").tap { _1.response = res }
27
55
  when Net::HTTPBadRequest
28
56
  reason = body.dig("error", "details", 0, "reason")
29
57
  if reason == "API_KEY_INVALID"
30
- raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
58
+ LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
31
59
  else
32
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
60
+ LLM::Error.new("Unexpected response").tap { _1.response = res }
33
61
  end
34
62
  when Net::HTTPTooManyRequests
35
- raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
63
+ LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
36
64
  else
37
- raise LLM::Error.new { _1.response = res }, "Unexpected response"
65
+ LLM::Error.new("Unexpected response").tap { _1.response = res }
38
66
  end
39
67
  end
40
-
41
- private
42
-
43
- def body
44
- @body ||= LLM.json.load(res.body)
45
- end
46
68
  end
47
69
  end
@@ -18,10 +18,10 @@ class LLM::Gemini
18
18
  # require "llm"
19
19
  #
20
20
  # llm = LLM.gemini(key: ENV["KEY"])
21
- # bot = LLM::Bot.new(llm)
21
+ # ses = LLM::Session.new(llm)
22
22
  # file = llm.files.create(file: "/audio/haiku.mp3")
23
- # bot.chat ["Tell me about this file", file]
24
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
23
+ # ses.talk ["Tell me about this file", file]
24
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
25
25
  class Files
26
26
  ##
27
27
  # Returns a new Files object
@@ -46,8 +46,9 @@ 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 = execute(request: req)
50
- ResponseAdapter.adapt(res, type: :files)
49
+ res, span = execute(request: req, operation: "request")
50
+ res = ResponseAdapter.adapt(res, type: :files)
51
+ finish_trace(operation: "request", res:, span:)
51
52
  end
52
53
 
53
54
  ##
@@ -68,8 +69,9 @@ class LLM::Gemini
68
69
  req["X-Goog-Upload-Command"] = "upload, finalize"
69
70
  file.with_io do |io|
70
71
  set_body_stream(req, io)
71
- res = execute(request: req)
72
- ResponseAdapter.adapt(res, type: :file)
72
+ res, span = execute(request: req, operation: "request")
73
+ res = ResponseAdapter.adapt(res, type: :file)
74
+ finish_trace(operation: "request", res:, span:)
73
75
  end
74
76
  end
75
77
 
@@ -88,8 +90,9 @@ class LLM::Gemini
88
90
  file_id = file.respond_to?(:name) ? file.name : file.to_s
89
91
  query = URI.encode_www_form(params.merge!(key: key))
90
92
  req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
91
- res = execute(request: req)
92
- ResponseAdapter.adapt(res, type: :file)
93
+ res, span = execute(request: req, operation: "request")
94
+ res = ResponseAdapter.adapt(res, type: :file)
95
+ finish_trace(operation: "request", res:, span:)
93
96
  end
94
97
 
95
98
  ##
@@ -106,8 +109,9 @@ class LLM::Gemini
106
109
  file_id = file.respond_to?(:name) ? file.name : file.to_s
107
110
  query = URI.encode_www_form(params.merge!(key: key))
108
111
  req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
109
- res = execute(request: req)
110
- LLM::Response.new(res)
112
+ res, span = execute(request: req, operation: "request")
113
+ res = LLM::Response.new(res)
114
+ finish_trace(operation: "request", res:, span:)
111
115
  end
112
116
 
113
117
  ##
@@ -128,7 +132,8 @@ class LLM::Gemini
128
132
  req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
129
133
  req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
130
134
  req.body = LLM.json.dump(file: {display_name: File.basename(file.path)})
131
- res = execute(request: req)
135
+ res, span = execute(request: req, operation: "request")
136
+ finish_trace(operation: "request", res: LLM::Response.new(res), span:)
132
137
  res["x-goog-upload-url"]
133
138
  end
134
139
 
@@ -136,7 +141,7 @@ class LLM::Gemini
136
141
  @provider.instance_variable_get(:@key)
137
142
  end
138
143
 
139
- [:headers, :execute, :set_body_stream].each do |m|
144
+ [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
140
145
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
141
146
  end
142
147
  end
@@ -51,8 +51,9 @@ class LLM::Gemini
51
51
  instances: [{prompt:}]
52
52
  })
53
53
  req.body = body
54
- res = execute(request: req)
55
- ResponseAdapter.adapt(res, type: :image)
54
+ res, span = execute(request: req, operation: "request")
55
+ res = ResponseAdapter.adapt(res, type: :image)
56
+ finish_trace(operation: "request", model:, res:, span:)
56
57
  end
57
58
 
58
59
  ##
@@ -89,7 +90,7 @@ class LLM::Gemini
89
90
  @provider.instance_variable_get(:@key)
90
91
  end
91
92
 
92
- [:headers, :execute, :set_body_stream].each do |m|
93
+ [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
93
94
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
94
95
  end
95
96
  end
@@ -42,8 +42,9 @@ 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 = execute(request: req)
46
- ResponseAdapter.adapt(res, type: :models)
45
+ res, span = execute(request: req, operation: "request")
46
+ res = ResponseAdapter.adapt(res, type: :models)
47
+ finish_trace(operation: "request", res:, span:)
47
48
  end
48
49
 
49
50
  private
@@ -52,7 +53,7 @@ class LLM::Gemini
52
53
  @provider.instance_variable_get(:@key)
53
54
  end
54
55
 
55
- [:headers, :execute].each do |m|
56
+ [:headers, :execute, :finish_trace].each do |m|
56
57
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
57
58
  end
58
59
  end
@@ -14,9 +14,9 @@ module LLM
14
14
  # require "llm"
15
15
  #
16
16
  # llm = LLM.gemini(key: ENV["KEY"])
17
- # bot = LLM::Bot.new(llm)
18
- # bot.chat ["Tell me about this photo", File.open("/images/horse.jpg", "rb")]
19
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
17
+ # ses = LLM::Session.new(llm)
18
+ # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
19
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class Gemini < Provider
21
21
  require_relative "gemini/error_handler"
22
22
  require_relative "gemini/request_adapter"
@@ -49,8 +49,9 @@ 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 = execute(request: req)
53
- ResponseAdapter.adapt(res, type: :embedding)
52
+ res, span = execute(request: req, operation: "embeddings", model:)
53
+ res = ResponseAdapter.adapt(res, type: :embedding)
54
+ finish_trace(operation: "embeddings", model:, res:, span:)
54
55
  end
55
56
 
56
57
  ##
@@ -66,9 +67,10 @@ module LLM
66
67
  def complete(prompt, params = {})
67
68
  params, stream, tools, role, model = normalize_complete_params(params)
68
69
  req = build_complete_request(prompt, params, role, model, stream)
69
- res = execute(request: req, stream: stream)
70
- ResponseAdapter.adapt(res, type: :completion)
70
+ res, span = execute(request: req, stream: stream, operation: "chat", model:)
71
+ res = ResponseAdapter.adapt(res, type: :completion)
71
72
  .extend(Module.new { define_method(:__tools__) { tools } })
73
+ finish_trace(operation: "chat", model:, res:, span:)
72
74
  end
73
75
 
74
76
  ##
@@ -103,12 +105,6 @@ module LLM
103
105
  LLM::Gemini::Models.new(self)
104
106
  end
105
107
 
106
- ##
107
- # @return (see LLM::Provider#assistant_role)
108
- def assistant_role
109
- "model"
110
- end
111
-
112
108
  ##
113
109
  # Returns the default model for chat completions
114
110
  # @see https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash gemini-2.5-flash
@@ -141,6 +137,33 @@ module LLM
141
137
  ResponseAdapter.adapt(complete(query, tools: [server_tools[:google_search]]), type: :web_search)
142
138
  end
143
139
 
140
+ ##
141
+ # @return [Symbol]
142
+ # Returns the providers user role
143
+ def user_role
144
+ :user
145
+ end
146
+
147
+ ##
148
+ # @return [Symbol]
149
+ # Returns the providers system role
150
+ def system_role
151
+ :user
152
+ end
153
+
154
+ ##
155
+ # @return [Symbol]
156
+ # Returns the providers developer role
157
+ def developer_role
158
+ :user
159
+ end
160
+
161
+ ##
162
+ # @return (see LLM::Provider#assistant_role)
163
+ def assistant_role
164
+ "model"
165
+ end
166
+
144
167
  private
145
168
 
146
169
  def headers
@@ -16,9 +16,9 @@ module LLM
16
16
  # require "llm"
17
17
  #
18
18
  # llm = LLM.llamacpp(key: nil)
19
- # bot = LLM::Bot.new(llm)
20
- # bot.chat ["Tell me about this photo", File.open("/images/frog.jpg", "rb")]
21
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
19
+ # ses = LLM::Session.new(llm)
20
+ # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
21
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
22
22
  class LlamaCpp < OpenAI
23
23
  ##
24
24
  # @param (see LLM::Provider#initialize)