llm.rb 4.1.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.md +186 -172
  4. data/lib/llm/agent.rb +49 -37
  5. data/lib/llm/bot.rb +57 -28
  6. data/lib/llm/function/tracing.rb +19 -0
  7. data/lib/llm/function.rb +16 -3
  8. data/lib/llm/json_adapter.rb +1 -1
  9. data/lib/llm/message.rb +7 -0
  10. data/lib/llm/prompt.rb +85 -0
  11. data/lib/llm/provider.rb +56 -10
  12. data/lib/llm/providers/anthropic/error_handler.rb +27 -5
  13. data/lib/llm/providers/anthropic/files.rb +22 -16
  14. data/lib/llm/providers/anthropic/models.rb +4 -3
  15. data/lib/llm/providers/anthropic.rb +6 -5
  16. data/lib/llm/providers/deepseek.rb +3 -3
  17. data/lib/llm/providers/gemini/error_handler.rb +34 -12
  18. data/lib/llm/providers/gemini/files.rb +18 -13
  19. data/lib/llm/providers/gemini/images.rb +4 -3
  20. data/lib/llm/providers/gemini/models.rb +4 -3
  21. data/lib/llm/providers/gemini.rb +9 -7
  22. data/lib/llm/providers/llamacpp.rb +3 -3
  23. data/lib/llm/providers/ollama/error_handler.rb +28 -6
  24. data/lib/llm/providers/ollama/models.rb +4 -3
  25. data/lib/llm/providers/ollama.rb +9 -7
  26. data/lib/llm/providers/openai/audio.rb +10 -7
  27. data/lib/llm/providers/openai/error_handler.rb +41 -14
  28. data/lib/llm/providers/openai/files.rb +19 -14
  29. data/lib/llm/providers/openai/images.rb +10 -7
  30. data/lib/llm/providers/openai/models.rb +4 -3
  31. data/lib/llm/providers/openai/moderations.rb +4 -3
  32. data/lib/llm/providers/openai/responses.rb +10 -7
  33. data/lib/llm/providers/openai/vector_stores.rb +34 -23
  34. data/lib/llm/providers/openai.rb +9 -7
  35. data/lib/llm/providers/xai.rb +3 -3
  36. data/lib/llm/providers/zai.rb +2 -2
  37. data/lib/llm/schema/object.rb +2 -2
  38. data/lib/llm/schema.rb +16 -2
  39. data/lib/llm/server_tool.rb +3 -3
  40. data/lib/llm/session.rb +3 -0
  41. data/lib/llm/tracer/logger.rb +192 -0
  42. data/lib/llm/tracer/null.rb +49 -0
  43. data/lib/llm/tracer/telemetry.rb +255 -0
  44. data/lib/llm/tracer.rb +134 -0
  45. data/lib/llm/version.rb +1 -1
  46. data/lib/llm.rb +4 -3
  47. data/llm.gemspec +4 -1
  48. metadata +38 -3
  49. data/lib/llm/builder.rb +0 -79
@@ -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
  ##
@@ -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)
@@ -10,10 +10,21 @@ class LLM::Ollama
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
- # @return [LLM::OpenAI::ErrorHandler]
16
- def initialize(res)
24
+ # @return [LLM::Ollama::ErrorHandler]
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::Ollama
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
@@ -43,13 +43,14 @@ 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 = execute(request: req)
47
- LLM::Response.new(res)
46
+ res, span = execute(request: req, operation: "request")
47
+ res = LLM::Response.new(res)
48
+ finish_trace(operation: "request", res:, span:)
48
49
  end
49
50
 
50
51
  private
51
52
 
52
- [:headers, :execute].each do |m|
53
+ [:headers, :execute, :finish_trace].each do |m|
53
54
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
54
55
  end
55
56
  end
@@ -12,9 +12,9 @@ module LLM
12
12
  # require "llm"
13
13
  #
14
14
  # llm = LLM.ollama(key: nil)
15
- # bot = LLM::Bot.new(llm, model: "llava")
16
- # bot.chat ["Tell me about this image", File.open("/images/parrot.png", "rb")]
17
- # bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
15
+ # ses = LLM::Session.new(llm, model: "llava")
16
+ # ses.talk ["Tell me about this image", ses.local_file("/images/photo.png")]
17
+ # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
18
18
  class Ollama < Provider
19
19
  require_relative "ollama/error_handler"
20
20
  require_relative "ollama/request_adapter"
@@ -43,8 +43,9 @@ 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 = execute(request: req)
47
- ResponseAdapter.adapt(res, type: :embedding)
46
+ res, span = execute(request: req, operation: "embeddings", model:)
47
+ res = ResponseAdapter.adapt(res, type: :embedding)
48
+ finish_trace(operation: "embeddings", model:, res:, span:)
48
49
  end
49
50
 
50
51
  ##
@@ -60,9 +61,10 @@ module LLM
60
61
  def complete(prompt, params = {})
61
62
  params, stream, tools, role = normalize_complete_params(params)
62
63
  req = build_complete_request(prompt, params, role)
63
- res = execute(request: req, stream: stream)
64
- ResponseAdapter.adapt(res, type: :completion)
64
+ res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
65
+ res = ResponseAdapter.adapt(res, type: :completion)
65
66
  .extend(Module.new { define_method(:__tools__) { tools } })
67
+ finish_trace(operation: "chat", model: params[:model], res:, span:)
66
68
  end
67
69
 
68
70
  ##
@@ -35,8 +35,9 @@ 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 = execute(request: req) { _1.read_body { |chunk| io << chunk } }
39
- LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
38
+ res, span = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
39
+ res = LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
40
+ finish_trace(operation: "request", model:, res:, span:)
40
41
  end
41
42
 
42
43
  ##
@@ -56,8 +57,9 @@ class LLM::OpenAI
56
57
  req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
57
58
  req["content-type"] = multi.content_type
58
59
  set_body_stream(req, multi.body)
59
- res = execute(request: req)
60
- LLM::Response.new(res)
60
+ res, span = execute(request: req, operation: "request")
61
+ res = LLM::Response.new(res)
62
+ finish_trace(operation: "request", model:, res:, span:)
61
63
  end
62
64
 
63
65
  ##
@@ -78,13 +80,14 @@ class LLM::OpenAI
78
80
  req = Net::HTTP::Post.new("/v1/audio/translations", headers)
79
81
  req["content-type"] = multi.content_type
80
82
  set_body_stream(req, multi.body)
81
- res = execute(request: req)
82
- LLM::Response.new(res)
83
+ res, span = execute(request: req, operation: "request")
84
+ res = LLM::Response.new(res)
85
+ finish_trace(operation: "request", model:, res:, span:)
83
86
  end
84
87
 
85
88
  private
86
89
 
87
- [:headers, :execute, :set_body_stream].each do |m|
90
+ [:headers, :execute, :set_body_stream, :finish_trace].each do |m|
88
91
  define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
89
92
  end
90
93
  end
@@ -10,10 +10,21 @@ class LLM::OpenAI
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::OpenAI::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,36 +32,52 @@ class LLM::OpenAI
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::HTTPUnauthorized
28
- raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
56
+ LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
29
57
  when Net::HTTPTooManyRequests
30
- raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
58
+ LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
31
59
  else
32
60
  error = body["error"] || {}
33
61
  case error["type"]
34
62
  when "invalid_request_error" then handle_invalid_request(error)
35
- when "server_error" then raise LLM::ServerError.new { _1.response = res }, error["message"]
36
- else raise LLM::Error.new { _1.response = res }, error["message"] || "Unexpected response"
63
+ when "server_error"
64
+ LLM::ServerError.new(error["message"]).tap { _1.response = res }
65
+ else
66
+ LLM::Error.new(error["message"] || "Unexpected response").tap { _1.response = res }
37
67
  end
38
68
  end
39
69
  end
40
70
 
41
- private
42
-
71
+ ##
72
+ # @param [Exception] error
73
+ # @return [LLM::Error]
43
74
  def handle_invalid_request(error)
44
75
  case error["code"]
45
76
  when "context_length_exceeded"
46
- raise LLM::ContextWindowError.new { _1.response = res }, error["message"]
77
+ LLM::ContextWindowError.new(error["message"]).tap { _1.response = res }
47
78
  else
48
- raise LLM::InvalidRequestError.new { _1.response = res }, error["message"]
79
+ LLM::InvalidRequestError.new(error["message"]).tap { _1.response = res }
49
80
  end
50
81
  end
51
-
52
- def body
53
- @body ||= LLM.json.load(res.body)
54
- end
55
82
  end
56
83
  end