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.
- checksums.yaml +4 -4
- data/LICENSE +2 -2
- data/README.md +226 -192
- data/lib/llm/agent.rb +226 -0
- data/lib/llm/bot.rb +57 -28
- data/lib/llm/error.rb +4 -0
- data/lib/llm/function/tracing.rb +19 -0
- data/lib/llm/function.rb +16 -3
- data/lib/llm/json_adapter.rb +1 -1
- data/lib/llm/message.rb +7 -0
- data/lib/llm/prompt.rb +85 -0
- data/lib/llm/provider.rb +74 -10
- data/lib/llm/providers/anthropic/error_handler.rb +27 -5
- data/lib/llm/providers/anthropic/files.rb +22 -16
- data/lib/llm/providers/anthropic/models.rb +4 -3
- data/lib/llm/providers/anthropic.rb +6 -5
- data/lib/llm/providers/deepseek.rb +3 -3
- data/lib/llm/providers/gemini/error_handler.rb +34 -12
- data/lib/llm/providers/gemini/files.rb +18 -13
- data/lib/llm/providers/gemini/images.rb +4 -3
- data/lib/llm/providers/gemini/models.rb +4 -3
- data/lib/llm/providers/gemini.rb +36 -13
- data/lib/llm/providers/llamacpp.rb +3 -3
- data/lib/llm/providers/ollama/error_handler.rb +28 -6
- data/lib/llm/providers/ollama/models.rb +4 -3
- data/lib/llm/providers/ollama.rb +9 -7
- data/lib/llm/providers/openai/audio.rb +10 -7
- data/lib/llm/providers/openai/error_handler.rb +41 -14
- data/lib/llm/providers/openai/files.rb +19 -14
- data/lib/llm/providers/openai/images.rb +10 -7
- data/lib/llm/providers/openai/models.rb +4 -3
- data/lib/llm/providers/openai/moderations.rb +4 -3
- data/lib/llm/providers/openai/responses.rb +10 -7
- data/lib/llm/providers/openai/vector_stores.rb +34 -23
- data/lib/llm/providers/openai.rb +9 -7
- data/lib/llm/providers/xai.rb +3 -3
- data/lib/llm/providers/zai.rb +2 -2
- data/lib/llm/schema/object.rb +2 -2
- data/lib/llm/schema.rb +16 -2
- data/lib/llm/server_tool.rb +3 -3
- data/lib/llm/session.rb +3 -0
- data/lib/llm/tracer/logger.rb +192 -0
- data/lib/llm/tracer/null.rb +49 -0
- data/lib/llm/tracer/telemetry.rb +255 -0
- data/lib/llm/tracer.rb +134 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +5 -3
- data/llm.gemspec +4 -1
- metadata +39 -3
- 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.
|
|
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::
|
|
95
|
+
# @return [LLM::Session]
|
|
95
96
|
def chat(prompt, params = {})
|
|
96
97
|
role = params.delete(:role)
|
|
97
|
-
LLM::
|
|
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::
|
|
106
|
+
# @return [LLM::Session]
|
|
106
107
|
def respond(prompt, params = {})
|
|
107
108
|
role = params.delete(:role)
|
|
108
|
-
LLM::
|
|
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
|
-
|
|
48
|
+
LLM::ServerError.new("Server error").tap { _1.response = res }
|
|
27
49
|
when Net::HTTPUnauthorized
|
|
28
|
-
|
|
50
|
+
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
29
51
|
when Net::HTTPTooManyRequests
|
|
30
|
-
|
|
52
|
+
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
31
53
|
else
|
|
32
|
-
|
|
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
|
-
#
|
|
13
|
+
# ses = LLM::Session.new(llm)
|
|
14
14
|
# file = llm.files.create file: "/books/goodread.pdf"
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
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
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
31
59
|
else
|
|
32
|
-
|
|
60
|
+
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
33
61
|
end
|
|
34
62
|
when Net::HTTPTooManyRequests
|
|
35
|
-
|
|
63
|
+
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
36
64
|
else
|
|
37
|
-
|
|
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
|
-
#
|
|
21
|
+
# ses = LLM::Session.new(llm)
|
|
22
22
|
# file = llm.files.create(file: "/audio/haiku.mp3")
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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
|
data/lib/llm/providers/gemini.rb
CHANGED
|
@@ -14,9 +14,9 @@ module LLM
|
|
|
14
14
|
# require "llm"
|
|
15
15
|
#
|
|
16
16
|
# llm = LLM.gemini(key: ENV["KEY"])
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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)
|