llm.rb 4.3.1 → 4.5.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/README.md +37 -13
- data/lib/llm/provider.rb +30 -24
- data/lib/llm/providers/anthropic/files.rb +19 -13
- data/lib/llm/providers/anthropic/models.rb +4 -3
- data/lib/llm/providers/anthropic.rb +11 -8
- data/lib/llm/providers/gemini/files.rb +16 -11
- data/lib/llm/providers/gemini/images.rb +4 -3
- data/lib/llm/providers/gemini/models.rb +4 -3
- data/lib/llm/providers/gemini/request_adapter/completion.rb +1 -1
- data/lib/llm/providers/gemini.rb +11 -7
- data/lib/llm/providers/ollama/models.rb +4 -3
- data/lib/llm/providers/ollama.rb +12 -8
- data/lib/llm/providers/openai/audio.rb +10 -7
- data/lib/llm/providers/openai/files.rb +16 -11
- 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 +12 -8
- data/lib/llm/tracer/telemetry.rb +84 -3
- data/lib/llm/tracer.rb +25 -0
- data/lib/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8252114d7ab58f00fd2d14389e932cfa9b2f0a71d96d9ea2c261f1f8b67d721
|
|
4
|
+
data.tar.gz: 8c00745ba750d0e271d8a4e5d5d9418a13556847c181e912aa8a8a1e7a9344b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b45173644a3803db844c1cad679a14ec647e058bd4517ce0160b7f2f352480877375748a4abb727007d3c76e7b977a6c106a5bfa438575f69ef8cd60a5612d2
|
|
7
|
+
data.tar.gz: bebdb107819b1410bc6644529ad0f56544deadbae9661ab8ed2c84a420c74097b357113650de7ed7da1d0b388c601a9b7e60e30d887b62d72e7cfb84f3f0dd1a
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<p align="center">
|
|
5
5
|
<a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
|
|
6
6
|
<a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
|
|
7
|
-
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.
|
|
7
|
+
<a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.5.0-green.svg?" alt="Version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
## About
|
|
@@ -177,11 +177,47 @@ end
|
|
|
177
177
|
ses.talk(prompt)
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
#### Threads
|
|
181
|
+
|
|
182
|
+
llm.rb is designed for threaded environments with throughput in mind.
|
|
183
|
+
Locks are used selectively, and localized state is preferred wherever
|
|
184
|
+
possible. Blanket locking across every class would help guarantee
|
|
185
|
+
correctness but it would also add contention, reduce throughput,
|
|
186
|
+
and increase complexity.
|
|
187
|
+
|
|
188
|
+
That's why we decided to optimize for both correctness and throughput
|
|
189
|
+
instead. An important part of that design is guaranteeing that
|
|
190
|
+
[LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
|
|
191
|
+
is safe to share across threads. [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html) and
|
|
192
|
+
[LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html) are
|
|
193
|
+
stateful objects that should be kept local to a single thread. So the
|
|
194
|
+
recommended pattern is to keep one session or agent per thread,
|
|
195
|
+
and share a provider across multiple threads:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
#!/usr/bin/env ruby
|
|
199
|
+
require "llm"
|
|
200
|
+
|
|
201
|
+
llm = LLM.openai(key: ENV["KEY"], persistent: true)
|
|
202
|
+
schema = llm.schema.object(answer: llm.schema.integer.required)
|
|
203
|
+
|
|
204
|
+
vals = 10.times.map do |x|
|
|
205
|
+
Thread.new do
|
|
206
|
+
ses = LLM::Session.new(llm, schema:)
|
|
207
|
+
res = ses.talk "#{x} + 5 = ?"
|
|
208
|
+
res.messages.find(&:assistant?).content!
|
|
209
|
+
end
|
|
210
|
+
end.map(&:value)
|
|
211
|
+
|
|
212
|
+
vals.each { |val| puts val }
|
|
213
|
+
```
|
|
214
|
+
|
|
180
215
|
## Features
|
|
181
216
|
|
|
182
217
|
#### General
|
|
183
218
|
- ✅ Unified API across providers
|
|
184
219
|
- 📦 Zero runtime deps (stdlib-only)
|
|
220
|
+
- 🧵 Thread-safe providers for multi-threaded workloads
|
|
185
221
|
- 🧩 Pluggable JSON adapters (JSON, Oj, Yajl, etc)
|
|
186
222
|
- 🧱 Builtin tracer API ([LLM::Tracer](https://0x1eef.github.io/x/llm.rb/LLM/Tracer.html))
|
|
187
223
|
|
|
@@ -438,18 +474,6 @@ ses2.restore(string: json)
|
|
|
438
474
|
ses2.talk "Howdy partner. I'm back"
|
|
439
475
|
```
|
|
440
476
|
|
|
441
|
-
#### Thread Safety
|
|
442
|
-
|
|
443
|
-
The llm.rb library is thread-safe and can be used in a multi-threaded
|
|
444
|
-
environments but it is important to keep in mind that the
|
|
445
|
-
[LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
|
|
446
|
-
and
|
|
447
|
-
[LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html)
|
|
448
|
-
classes should be instantiated once per thread, and not shared
|
|
449
|
-
between threads. Generally the library tries to avoid global or
|
|
450
|
-
shared state but where it exists reentrant locks are used to
|
|
451
|
-
ensure thread-safety.
|
|
452
|
-
|
|
453
477
|
### Tools
|
|
454
478
|
|
|
455
479
|
#### LLM::Function
|
data/lib/llm/provider.rb
CHANGED
|
@@ -36,9 +36,11 @@ class LLM::Provider
|
|
|
36
36
|
@port = port
|
|
37
37
|
@timeout = timeout
|
|
38
38
|
@ssl = ssl
|
|
39
|
-
@client = persistent ? persistent_client :
|
|
39
|
+
@client = persistent ? persistent_client : nil
|
|
40
40
|
@tracer = LLM::Tracer::Null.new(self)
|
|
41
41
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
42
|
+
@headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
|
|
43
|
+
@monitor = Monitor.new
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
##
|
|
@@ -181,7 +183,7 @@ class LLM::Provider
|
|
|
181
183
|
# Returns an object that can generate a JSON schema
|
|
182
184
|
# @return [LLM::Schema]
|
|
183
185
|
def schema
|
|
184
|
-
|
|
186
|
+
LLM::Schema.new
|
|
185
187
|
end
|
|
186
188
|
|
|
187
189
|
##
|
|
@@ -195,7 +197,9 @@ class LLM::Provider
|
|
|
195
197
|
# @return [LLM::Provider]
|
|
196
198
|
# Returns self
|
|
197
199
|
def with(headers:)
|
|
198
|
-
|
|
200
|
+
lock do
|
|
201
|
+
tap { @headers.merge!(headers) }
|
|
202
|
+
end
|
|
199
203
|
end
|
|
200
204
|
|
|
201
205
|
##
|
|
@@ -276,10 +280,12 @@ class LLM::Provider
|
|
|
276
280
|
# A tracer
|
|
277
281
|
# @return [void]
|
|
278
282
|
def tracer=(tracer)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
lock do
|
|
284
|
+
@tracer = if tracer.nil?
|
|
285
|
+
LLM::Tracer::Null.new(self)
|
|
286
|
+
else
|
|
287
|
+
tracer
|
|
288
|
+
end
|
|
283
289
|
end
|
|
284
290
|
end
|
|
285
291
|
|
|
@@ -335,10 +341,12 @@ class LLM::Provider
|
|
|
335
341
|
# When there is a network error at the operating system level
|
|
336
342
|
# @return [Net::HTTPResponse]
|
|
337
343
|
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
|
|
338
|
-
|
|
339
|
-
|
|
344
|
+
tracer = @tracer
|
|
345
|
+
span = tracer.on_request_start(operation:, model:)
|
|
346
|
+
http = client || transient_client
|
|
347
|
+
args = (Net::HTTP === http) ? [request] : [URI.join(base_uri, request.path), request]
|
|
340
348
|
res = if stream
|
|
341
|
-
|
|
349
|
+
http.request(*args) do |res|
|
|
342
350
|
handler = event_handler.new stream_parser.new(stream)
|
|
343
351
|
parser = LLM::EventStream::Parser.new
|
|
344
352
|
parser.register(handler)
|
|
@@ -352,10 +360,10 @@ class LLM::Provider
|
|
|
352
360
|
parser&.free
|
|
353
361
|
end
|
|
354
362
|
else
|
|
355
|
-
b ?
|
|
356
|
-
|
|
363
|
+
b ? http.request(*args) { (Net::HTTPSuccess === _1) ? b.call(_1) : _1 } :
|
|
364
|
+
http.request(*args)
|
|
357
365
|
end
|
|
358
|
-
[handle_response(res, span), span]
|
|
366
|
+
[handle_response(res, tracer, span), span, tracer]
|
|
359
367
|
end
|
|
360
368
|
|
|
361
369
|
##
|
|
@@ -365,14 +373,18 @@ class LLM::Provider
|
|
|
365
373
|
# @param [Object, nil] span
|
|
366
374
|
# The span
|
|
367
375
|
# @return [Net::HTTPResponse]
|
|
368
|
-
def handle_response(res, span)
|
|
376
|
+
def handle_response(res, tracer, span)
|
|
369
377
|
case res
|
|
370
378
|
when Net::HTTPOK then res.body = parse_response(res)
|
|
371
|
-
else error_handler.new(
|
|
379
|
+
else error_handler.new(tracer, span, res).raise_error!
|
|
372
380
|
end
|
|
373
381
|
res
|
|
374
382
|
end
|
|
375
383
|
|
|
384
|
+
##
|
|
385
|
+
# Parse a HTTP response
|
|
386
|
+
# @param [Net::HTTPResponse] res
|
|
387
|
+
# @return [LLM::Object, String]
|
|
376
388
|
def parse_response(res)
|
|
377
389
|
case res["content-type"]
|
|
378
390
|
when %r|\Aapplication/json\s*| then LLM::Object.from(LLM.json.load(res.body))
|
|
@@ -417,14 +429,8 @@ class LLM::Provider
|
|
|
417
429
|
end
|
|
418
430
|
|
|
419
431
|
##
|
|
420
|
-
#
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
# @param [LLM::Response] res
|
|
424
|
-
# @param [Object, nil] span
|
|
425
|
-
# @return [LLM::Response]
|
|
426
|
-
def finish_trace(operation:, res:, model: nil, span: nil)
|
|
427
|
-
@tracer.on_request_finish(operation:, model:, res:, span:)
|
|
428
|
-
res
|
|
432
|
+
# @api private
|
|
433
|
+
def lock(&)
|
|
434
|
+
@monitor.synchronize(&)
|
|
429
435
|
end
|
|
430
436
|
end
|
|
@@ -38,9 +38,10 @@ 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, span = execute(request: req, operation: "request")
|
|
41
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
42
42
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
43
|
-
|
|
43
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
44
|
+
res
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
##
|
|
@@ -58,9 +59,10 @@ class LLM::Anthropic
|
|
|
58
59
|
req = Net::HTTP::Post.new("/v1/files", headers)
|
|
59
60
|
req["content-type"] = multi.content_type
|
|
60
61
|
set_body_stream(req, multi.body)
|
|
61
|
-
res, span = execute(request: req, operation: "request")
|
|
62
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
62
63
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
63
|
-
|
|
64
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
65
|
+
res
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
##
|
|
@@ -78,9 +80,10 @@ class LLM::Anthropic
|
|
|
78
80
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
79
81
|
query = URI.encode_www_form(params)
|
|
80
82
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
81
|
-
res, span = execute(request: req, operation: "request")
|
|
83
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
82
84
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
83
|
-
|
|
85
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
86
|
+
res
|
|
84
87
|
end
|
|
85
88
|
|
|
86
89
|
##
|
|
@@ -98,9 +101,10 @@ class LLM::Anthropic
|
|
|
98
101
|
query = URI.encode_www_form(params)
|
|
99
102
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
100
103
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
101
|
-
res, span = execute(request: req, operation: "request")
|
|
104
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
102
105
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
103
|
-
|
|
106
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
107
|
+
res
|
|
104
108
|
end
|
|
105
109
|
alias_method :retrieve_metadata, :get_metadata
|
|
106
110
|
|
|
@@ -117,9 +121,10 @@ class LLM::Anthropic
|
|
|
117
121
|
def delete(file:)
|
|
118
122
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
119
123
|
req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
|
|
120
|
-
res, span = execute(request: req, operation: "request")
|
|
124
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
121
125
|
res = LLM::Response.new(res)
|
|
122
|
-
|
|
126
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
127
|
+
res
|
|
123
128
|
end
|
|
124
129
|
|
|
125
130
|
##
|
|
@@ -142,9 +147,10 @@ class LLM::Anthropic
|
|
|
142
147
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
143
148
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
|
|
144
149
|
io = StringIO.new("".b)
|
|
145
|
-
res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
150
|
+
res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
146
151
|
res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
147
|
-
|
|
152
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
153
|
+
res
|
|
148
154
|
end
|
|
149
155
|
|
|
150
156
|
private
|
|
@@ -153,7 +159,7 @@ class LLM::Anthropic
|
|
|
153
159
|
@provider.instance_variable_get(:@key)
|
|
154
160
|
end
|
|
155
161
|
|
|
156
|
-
[:headers, :execute, :set_body_stream
|
|
162
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
157
163
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
158
164
|
end
|
|
159
165
|
end
|
|
@@ -40,14 +40,15 @@ 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, span = execute(request: req, operation: "request")
|
|
43
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
44
44
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
45
|
-
|
|
45
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
46
|
+
res
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
private
|
|
49
50
|
|
|
50
|
-
[:headers, :execute
|
|
51
|
+
[:headers, :execute].each do |m|
|
|
51
52
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
52
53
|
end
|
|
53
54
|
end
|
|
@@ -43,10 +43,11 @@ 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, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
46
|
+
res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
47
47
|
res = ResponseAdapter.adapt(res, type: :completion)
|
|
48
48
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
49
|
-
|
|
49
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
50
|
+
res
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
##
|
|
@@ -110,12 +111,14 @@ module LLM
|
|
|
110
111
|
private
|
|
111
112
|
|
|
112
113
|
def headers
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
lock do
|
|
115
|
+
(@headers || {}).merge(
|
|
116
|
+
"Content-Type" => "application/json",
|
|
117
|
+
"x-api-key" => @key,
|
|
118
|
+
"anthropic-version" => "2023-06-01",
|
|
119
|
+
"anthropic-beta" => "files-api-2025-04-14"
|
|
120
|
+
)
|
|
121
|
+
end
|
|
119
122
|
end
|
|
120
123
|
|
|
121
124
|
def stream_parser
|
|
@@ -46,9 +46,10 @@ 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, span = execute(request: req, operation: "request")
|
|
49
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
50
50
|
res = ResponseAdapter.adapt(res, type: :files)
|
|
51
|
-
|
|
51
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
52
|
+
res
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
##
|
|
@@ -69,9 +70,10 @@ class LLM::Gemini
|
|
|
69
70
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
|
70
71
|
file.with_io do |io|
|
|
71
72
|
set_body_stream(req, io)
|
|
72
|
-
res, span = execute(request: req, operation: "request")
|
|
73
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
73
74
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
74
|
-
|
|
75
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
76
|
+
res
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
@@ -90,9 +92,10 @@ class LLM::Gemini
|
|
|
90
92
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
91
93
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
92
94
|
req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
|
|
93
|
-
res, span = execute(request: req, operation: "request")
|
|
95
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
94
96
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
95
|
-
|
|
97
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
98
|
+
res
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
##
|
|
@@ -109,9 +112,10 @@ class LLM::Gemini
|
|
|
109
112
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
|
110
113
|
query = URI.encode_www_form(params.merge!(key: key))
|
|
111
114
|
req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
|
|
112
|
-
res, span = execute(request: req, operation: "request")
|
|
115
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
113
116
|
res = LLM::Response.new(res)
|
|
114
|
-
|
|
117
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
118
|
+
res
|
|
115
119
|
end
|
|
116
120
|
|
|
117
121
|
##
|
|
@@ -132,8 +136,9 @@ class LLM::Gemini
|
|
|
132
136
|
req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
|
|
133
137
|
req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
|
|
134
138
|
req.body = LLM.json.dump({file: {display_name: File.basename(file.path)}})
|
|
135
|
-
res, span = execute(request: req, operation: "request")
|
|
136
|
-
|
|
139
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
140
|
+
trace_res = LLM::Response.new(res)
|
|
141
|
+
tracer.on_request_finish(operation: "request", res: trace_res, span:)
|
|
137
142
|
res["x-goog-upload-url"]
|
|
138
143
|
end
|
|
139
144
|
|
|
@@ -141,7 +146,7 @@ class LLM::Gemini
|
|
|
141
146
|
@provider.instance_variable_get(:@key)
|
|
142
147
|
end
|
|
143
148
|
|
|
144
|
-
[:headers, :execute, :set_body_stream
|
|
149
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
145
150
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
146
151
|
end
|
|
147
152
|
end
|
|
@@ -51,9 +51,10 @@ class LLM::Gemini
|
|
|
51
51
|
instances: [{prompt:}]
|
|
52
52
|
})
|
|
53
53
|
req.body = body
|
|
54
|
-
res, span = execute(request: req, operation: "request")
|
|
54
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
55
55
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
56
|
-
|
|
56
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
57
|
+
res
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
##
|
|
@@ -90,7 +91,7 @@ class LLM::Gemini
|
|
|
90
91
|
@provider.instance_variable_get(:@key)
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
[:headers, :execute, :set_body_stream
|
|
94
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
94
95
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
95
96
|
end
|
|
96
97
|
end
|
|
@@ -42,9 +42,10 @@ 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, span = execute(request: req, operation: "request")
|
|
45
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
46
46
|
res = ResponseAdapter.adapt(res, type: :models)
|
|
47
|
-
|
|
47
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
48
|
+
res
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
private
|
|
@@ -53,7 +54,7 @@ class LLM::Gemini
|
|
|
53
54
|
@provider.instance_variable_get(:@key)
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
[:headers, :execute
|
|
57
|
+
[:headers, :execute].each do |m|
|
|
57
58
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
58
59
|
end
|
|
59
60
|
end
|
|
@@ -48,7 +48,7 @@ module LLM::Gemini::RequestAdapter
|
|
|
48
48
|
def adapt_object(object)
|
|
49
49
|
case object.kind
|
|
50
50
|
when :image_url
|
|
51
|
-
[{file_data: {
|
|
51
|
+
[{file_data: {file_uri: object.value.to_s}}]
|
|
52
52
|
when :local_file
|
|
53
53
|
file = object.value
|
|
54
54
|
[{inline_data: {mime_type: file.mime_type, data: file.to_b64}}]
|
data/lib/llm/providers/gemini.rb
CHANGED
|
@@ -49,9 +49,10 @@ 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, span = execute(request: req, operation: "embeddings", model:)
|
|
52
|
+
res, span, tracer = execute(request: req, operation: "embeddings", model:)
|
|
53
53
|
res = ResponseAdapter.adapt(res, type: :embedding)
|
|
54
|
-
|
|
54
|
+
tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
|
|
55
|
+
res
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
##
|
|
@@ -67,10 +68,11 @@ module LLM
|
|
|
67
68
|
def complete(prompt, params = {})
|
|
68
69
|
params, stream, tools, role, model = normalize_complete_params(params)
|
|
69
70
|
req = build_complete_request(prompt, params, role, model, stream)
|
|
70
|
-
res, span = execute(request: req, stream: stream, operation: "chat", model:)
|
|
71
|
+
res, span, tracer = execute(request: req, stream: stream, operation: "chat", model:)
|
|
71
72
|
res = ResponseAdapter.adapt(res, type: :completion)
|
|
72
73
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
73
|
-
|
|
74
|
+
tracer.on_request_finish(operation: "chat", model:, res:, span:)
|
|
75
|
+
res
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
##
|
|
@@ -167,9 +169,11 @@ module LLM
|
|
|
167
169
|
private
|
|
168
170
|
|
|
169
171
|
def headers
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
lock do
|
|
173
|
+
(@headers || {}).merge(
|
|
174
|
+
"Content-Type" => "application/json"
|
|
175
|
+
)
|
|
176
|
+
end
|
|
173
177
|
end
|
|
174
178
|
|
|
175
179
|
def stream_parser
|
|
@@ -43,14 +43,15 @@ 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, span = execute(request: req, operation: "request")
|
|
46
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
47
47
|
res = LLM::Response.new(res)
|
|
48
|
-
|
|
48
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
49
|
+
res
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
private
|
|
52
53
|
|
|
53
|
-
[:headers, :execute
|
|
54
|
+
[:headers, :execute].each do |m|
|
|
54
55
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
55
56
|
end
|
|
56
57
|
end
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -43,9 +43,10 @@ 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, span = execute(request: req, operation: "embeddings", model:)
|
|
46
|
+
res, span, tracer = execute(request: req, operation: "embeddings", model:)
|
|
47
47
|
res = ResponseAdapter.adapt(res, type: :embedding)
|
|
48
|
-
|
|
48
|
+
tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
|
|
49
|
+
res
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
##
|
|
@@ -61,10 +62,11 @@ module LLM
|
|
|
61
62
|
def complete(prompt, params = {})
|
|
62
63
|
params, stream, tools, role = normalize_complete_params(params)
|
|
63
64
|
req = build_complete_request(prompt, params, role)
|
|
64
|
-
res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
65
|
+
res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
65
66
|
res = ResponseAdapter.adapt(res, type: :completion)
|
|
66
67
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
67
|
-
|
|
68
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
69
|
+
res
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
##
|
|
@@ -92,10 +94,12 @@ module LLM
|
|
|
92
94
|
private
|
|
93
95
|
|
|
94
96
|
def headers
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
lock do
|
|
98
|
+
(@headers || {}).merge(
|
|
99
|
+
"Content-Type" => "application/json",
|
|
100
|
+
"Authorization" => "Bearer #{@key}"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
def stream_parser
|
|
@@ -35,9 +35,10 @@ 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, span = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
|
|
38
|
+
res, span, tracer = execute(request: req, operation: "request") { _1.read_body { |chunk| io << chunk } }
|
|
39
39
|
res = LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
|
|
40
|
-
|
|
40
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
41
|
+
res
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
##
|
|
@@ -57,9 +58,10 @@ class LLM::OpenAI
|
|
|
57
58
|
req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
|
|
58
59
|
req["content-type"] = multi.content_type
|
|
59
60
|
set_body_stream(req, multi.body)
|
|
60
|
-
res, span = execute(request: req, operation: "request")
|
|
61
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
61
62
|
res = LLM::Response.new(res)
|
|
62
|
-
|
|
63
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
64
|
+
res
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
##
|
|
@@ -80,14 +82,15 @@ class LLM::OpenAI
|
|
|
80
82
|
req = Net::HTTP::Post.new("/v1/audio/translations", headers)
|
|
81
83
|
req["content-type"] = multi.content_type
|
|
82
84
|
set_body_stream(req, multi.body)
|
|
83
|
-
res, span = execute(request: req, operation: "request")
|
|
85
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
84
86
|
res = LLM::Response.new(res)
|
|
85
|
-
|
|
87
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
88
|
+
res
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
private
|
|
89
92
|
|
|
90
|
-
[:headers, :execute, :set_body_stream
|
|
93
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
91
94
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
92
95
|
end
|
|
93
96
|
end
|
|
@@ -41,9 +41,10 @@ class LLM::OpenAI
|
|
|
41
41
|
def all(**params)
|
|
42
42
|
query = URI.encode_www_form(params)
|
|
43
43
|
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
|
44
|
-
res, span = execute(request: req, operation: "request")
|
|
44
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
45
45
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
46
|
-
|
|
46
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
47
|
+
res
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
##
|
|
@@ -62,9 +63,10 @@ class LLM::OpenAI
|
|
|
62
63
|
req = Net::HTTP::Post.new("/v1/files", headers)
|
|
63
64
|
req["content-type"] = multi.content_type
|
|
64
65
|
set_body_stream(req, multi.body)
|
|
65
|
-
res, span = execute(request: req, operation: "request")
|
|
66
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
66
67
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
67
|
-
|
|
68
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
69
|
+
res
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
##
|
|
@@ -82,9 +84,10 @@ class LLM::OpenAI
|
|
|
82
84
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
83
85
|
query = URI.encode_www_form(params)
|
|
84
86
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
85
|
-
res, span = execute(request: req, operation: "request")
|
|
87
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
86
88
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
87
|
-
|
|
89
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
90
|
+
res
|
|
88
91
|
end
|
|
89
92
|
|
|
90
93
|
##
|
|
@@ -104,9 +107,10 @@ class LLM::OpenAI
|
|
|
104
107
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
105
108
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
|
|
106
109
|
io = StringIO.new("".b)
|
|
107
|
-
res, span = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
110
|
+
res, span, tracer = execute(request: req, operation: "request") { |res| res.read_body { |chunk| io << chunk } }
|
|
108
111
|
res = LLM::Response.new(res).tap { _1.define_singleton_method(:file) { io } }
|
|
109
|
-
|
|
112
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
113
|
+
res
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
##
|
|
@@ -122,14 +126,15 @@ class LLM::OpenAI
|
|
|
122
126
|
def delete(file:)
|
|
123
127
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
124
128
|
req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
|
|
125
|
-
res, span = execute(request: req, operation: "request")
|
|
129
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
126
130
|
res = LLM::Response.new(res)
|
|
127
|
-
|
|
131
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
132
|
+
res
|
|
128
133
|
end
|
|
129
134
|
|
|
130
135
|
private
|
|
131
136
|
|
|
132
|
-
[:headers, :execute, :set_body_stream
|
|
137
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
133
138
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
134
139
|
end
|
|
135
140
|
end
|
|
@@ -50,9 +50,10 @@ class LLM::OpenAI
|
|
|
50
50
|
def create(prompt:, model: "dall-e-3", **params)
|
|
51
51
|
req = Net::HTTP::Post.new("/v1/images/generations", headers)
|
|
52
52
|
req.body = LLM.json.dump({prompt:, n: 1, model:}.merge!(params))
|
|
53
|
-
res, span = execute(request: req, operation: "request")
|
|
53
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
54
54
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
55
|
-
|
|
55
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
56
|
+
res
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
##
|
|
@@ -73,9 +74,10 @@ class LLM::OpenAI
|
|
|
73
74
|
req = Net::HTTP::Post.new("/v1/images/variations", headers)
|
|
74
75
|
req["content-type"] = multi.content_type
|
|
75
76
|
set_body_stream(req, multi.body)
|
|
76
|
-
res, span = execute(request: req, operation: "request")
|
|
77
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
77
78
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
78
|
-
|
|
79
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
80
|
+
res
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
##
|
|
@@ -97,14 +99,15 @@ class LLM::OpenAI
|
|
|
97
99
|
req = Net::HTTP::Post.new("/v1/images/edits", headers)
|
|
98
100
|
req["content-type"] = multi.content_type
|
|
99
101
|
set_body_stream(req, multi.body)
|
|
100
|
-
res, span = execute(request: req, operation: "request")
|
|
102
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
101
103
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
102
|
-
|
|
104
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
105
|
+
res
|
|
103
106
|
end
|
|
104
107
|
|
|
105
108
|
private
|
|
106
109
|
|
|
107
|
-
[:headers, :execute, :set_body_stream
|
|
110
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
108
111
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
109
112
|
end
|
|
110
113
|
end
|
|
@@ -40,14 +40,15 @@ class LLM::OpenAI
|
|
|
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, span = execute(request: req, operation: "request")
|
|
43
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
44
44
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
45
|
-
|
|
45
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
46
|
+
res
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
private
|
|
49
50
|
|
|
50
|
-
[:headers, :execute, :set_body_stream
|
|
51
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
51
52
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
52
53
|
end
|
|
53
54
|
end
|
|
@@ -50,14 +50,15 @@ class LLM::OpenAI
|
|
|
50
50
|
req = Net::HTTP::Post.new("/v1/moderations", headers)
|
|
51
51
|
input = RequestAdapter::Moderation.new(input).adapt
|
|
52
52
|
req.body = LLM.json.dump({input:, model:}.merge!(params))
|
|
53
|
-
res, span = execute(request: req, operation: "request")
|
|
53
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
54
54
|
res = ResponseAdapter.adapt(res, type: :moderations)
|
|
55
|
-
|
|
55
|
+
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
56
|
+
res
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
private
|
|
59
60
|
|
|
60
|
-
[:headers, :execute
|
|
61
|
+
[:headers, :execute].each do |m|
|
|
61
62
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
62
63
|
end
|
|
63
64
|
end
|
|
@@ -44,10 +44,11 @@ class LLM::OpenAI
|
|
|
44
44
|
messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
|
45
45
|
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
46
46
|
set_body_stream(req, StringIO.new(body))
|
|
47
|
-
res, span = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
|
|
47
|
+
res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
|
|
48
48
|
res = ResponseAdapter.adapt(res, type: :responds)
|
|
49
49
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
50
|
-
|
|
50
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
51
|
+
res
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
##
|
|
@@ -60,9 +61,10 @@ class LLM::OpenAI
|
|
|
60
61
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
61
62
|
query = URI.encode_www_form(params)
|
|
62
63
|
req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
|
|
63
|
-
res, span = execute(request: req, operation: "request")
|
|
64
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
64
65
|
res = ResponseAdapter.adapt(res, type: :responds)
|
|
65
|
-
|
|
66
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
67
|
+
res
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
##
|
|
@@ -74,14 +76,15 @@ class LLM::OpenAI
|
|
|
74
76
|
def delete(response)
|
|
75
77
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
76
78
|
req = Net::HTTP::Delete.new("/v1/responses/#{response_id}", headers)
|
|
77
|
-
res, span = execute(request: req, operation: "request")
|
|
79
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
78
80
|
res = LLM::Response.new(res)
|
|
79
|
-
|
|
81
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
82
|
+
res
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
private
|
|
83
86
|
|
|
84
|
-
[:headers, :execute, :set_body_stream, :resolve_tools
|
|
87
|
+
[:headers, :execute, :set_body_stream, :resolve_tools].each do |m|
|
|
85
88
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
86
89
|
end
|
|
87
90
|
|
|
@@ -32,9 +32,10 @@ class LLM::OpenAI
|
|
|
32
32
|
def all(**params)
|
|
33
33
|
query = URI.encode_www_form(params)
|
|
34
34
|
req = Net::HTTP::Get.new("/v1/vector_stores?#{query}", headers)
|
|
35
|
-
res, span = execute(request: req, operation: "request")
|
|
35
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
36
36
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
37
|
-
|
|
37
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
38
|
+
res
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
##
|
|
@@ -48,9 +49,10 @@ class LLM::OpenAI
|
|
|
48
49
|
def create(name:, file_ids: nil, **params)
|
|
49
50
|
req = Net::HTTP::Post.new("/v1/vector_stores", headers)
|
|
50
51
|
req.body = LLM.json.dump(params.merge({name:, file_ids:}).compact)
|
|
51
|
-
res, span = execute(request: req, operation: "request")
|
|
52
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
52
53
|
res = LLM::Response.new(res)
|
|
53
|
-
|
|
54
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
55
|
+
res
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
##
|
|
@@ -71,9 +73,10 @@ class LLM::OpenAI
|
|
|
71
73
|
def get(vector:)
|
|
72
74
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
73
75
|
req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}", headers)
|
|
74
|
-
res, span = execute(request: req, operation: "request")
|
|
76
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
75
77
|
res = LLM::Response.new(res)
|
|
76
|
-
|
|
78
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
79
|
+
res
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
##
|
|
@@ -88,9 +91,10 @@ class LLM::OpenAI
|
|
|
88
91
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
89
92
|
req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}", headers)
|
|
90
93
|
req.body = LLM.json.dump(params.merge({name:}).compact)
|
|
91
|
-
res, span = execute(request: req, operation: "request")
|
|
94
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
92
95
|
res = LLM::Response.new(res)
|
|
93
|
-
|
|
96
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
97
|
+
res
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
##
|
|
@@ -102,9 +106,10 @@ class LLM::OpenAI
|
|
|
102
106
|
def delete(vector:)
|
|
103
107
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
104
108
|
req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}", headers)
|
|
105
|
-
res, span = execute(request: req, operation: "request")
|
|
109
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
106
110
|
res = LLM::Response.new(res)
|
|
107
|
-
|
|
111
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
112
|
+
res
|
|
108
113
|
end
|
|
109
114
|
|
|
110
115
|
##
|
|
@@ -119,9 +124,10 @@ class LLM::OpenAI
|
|
|
119
124
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
120
125
|
req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/search", headers)
|
|
121
126
|
req.body = LLM.json.dump(params.merge({query:}).compact)
|
|
122
|
-
res, span = execute(request: req, operation: "retrieval")
|
|
127
|
+
res, span, tracer = execute(request: req, operation: "retrieval")
|
|
123
128
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
124
|
-
|
|
129
|
+
tracer.on_request_finish(operation: "retrieval", res:, span:)
|
|
130
|
+
res
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
##
|
|
@@ -135,9 +141,10 @@ class LLM::OpenAI
|
|
|
135
141
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
136
142
|
query = URI.encode_www_form(params)
|
|
137
143
|
req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files?#{query}", headers)
|
|
138
|
-
res, span = execute(request: req, operation: "request")
|
|
144
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
139
145
|
res = ResponseAdapter.adapt(res, type: :enumerable)
|
|
140
|
-
|
|
146
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
147
|
+
res
|
|
141
148
|
end
|
|
142
149
|
|
|
143
150
|
##
|
|
@@ -154,9 +161,10 @@ class LLM::OpenAI
|
|
|
154
161
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
155
162
|
req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files", headers)
|
|
156
163
|
req.body = LLM.json.dump(params.merge({file_id:, attributes:}).compact)
|
|
157
|
-
res, span = execute(request: req, operation: "request")
|
|
164
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
158
165
|
res = LLM::Response.new(res)
|
|
159
|
-
|
|
166
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
167
|
+
res
|
|
160
168
|
end
|
|
161
169
|
alias_method :create_file, :add_file
|
|
162
170
|
|
|
@@ -184,9 +192,10 @@ class LLM::OpenAI
|
|
|
184
192
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
185
193
|
req = Net::HTTP::Post.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
|
|
186
194
|
req.body = LLM.json.dump(params.merge({attributes:}).compact)
|
|
187
|
-
res, span = execute(request: req, operation: "request")
|
|
195
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
188
196
|
res = LLM::Response.new(res)
|
|
189
|
-
|
|
197
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
198
|
+
res
|
|
190
199
|
end
|
|
191
200
|
|
|
192
201
|
##
|
|
@@ -201,9 +210,10 @@ class LLM::OpenAI
|
|
|
201
210
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
202
211
|
query = URI.encode_www_form(params)
|
|
203
212
|
req = Net::HTTP::Get.new("/v1/vector_stores/#{vector_id}/files/#{file_id}?#{query}", headers)
|
|
204
|
-
res, span = execute(request: req, operation: "request")
|
|
213
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
205
214
|
res = LLM::Response.new(res)
|
|
206
|
-
|
|
215
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
216
|
+
res
|
|
207
217
|
end
|
|
208
218
|
|
|
209
219
|
##
|
|
@@ -217,9 +227,10 @@ class LLM::OpenAI
|
|
|
217
227
|
vector_id = vector.respond_to?(:id) ? vector.id : vector
|
|
218
228
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
219
229
|
req = Net::HTTP::Delete.new("/v1/vector_stores/#{vector_id}/files/#{file_id}", headers)
|
|
220
|
-
res, span = execute(request: req, operation: "request")
|
|
230
|
+
res, span, tracer = execute(request: req, operation: "request")
|
|
221
231
|
res = LLM::Response.new(res)
|
|
222
|
-
|
|
232
|
+
tracer.on_request_finish(operation: "request", res:, span:)
|
|
233
|
+
res
|
|
223
234
|
end
|
|
224
235
|
|
|
225
236
|
##
|
|
@@ -248,7 +259,7 @@ class LLM::OpenAI
|
|
|
248
259
|
|
|
249
260
|
private
|
|
250
261
|
|
|
251
|
-
[:headers, :execute, :set_body_stream
|
|
262
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
|
252
263
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
253
264
|
end
|
|
254
265
|
end
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -47,9 +47,10 @@ module LLM
|
|
|
47
47
|
def embed(input, model: "text-embedding-3-small", **params)
|
|
48
48
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
|
49
49
|
req.body = LLM.json.dump({input:, model:}.merge!(params))
|
|
50
|
-
res, span = execute(request: req, operation: "embeddings", model:)
|
|
50
|
+
res, span, tracer = execute(request: req, operation: "embeddings", model:)
|
|
51
51
|
res = ResponseAdapter.adapt(res, type: :embedding)
|
|
52
|
-
|
|
52
|
+
tracer.on_request_finish(operation: "embeddings", model:, res:, span:)
|
|
53
|
+
res
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
##
|
|
@@ -65,10 +66,11 @@ module LLM
|
|
|
65
66
|
def complete(prompt, params = {})
|
|
66
67
|
params, stream, tools, role = normalize_complete_params(params)
|
|
67
68
|
req = build_complete_request(prompt, params, role)
|
|
68
|
-
res, span = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
69
|
+
res, span, tracer = execute(request: req, stream: stream, operation: "chat", model: params[:model])
|
|
69
70
|
res = ResponseAdapter.adapt(res, type: :completion)
|
|
70
71
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
71
|
-
|
|
72
|
+
tracer.on_request_finish(operation: "chat", model: params[:model], res:, span:)
|
|
73
|
+
res
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
##
|
|
@@ -181,10 +183,12 @@ module LLM
|
|
|
181
183
|
end
|
|
182
184
|
|
|
183
185
|
def headers
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
lock do
|
|
187
|
+
(@headers || {}).merge(
|
|
188
|
+
"Content-Type" => "application/json",
|
|
189
|
+
"Authorization" => "Bearer #{@key}"
|
|
190
|
+
)
|
|
191
|
+
end
|
|
188
192
|
end
|
|
189
193
|
|
|
190
194
|
def stream_parser
|
data/lib/llm/tracer/telemetry.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module LLM
|
|
4
6
|
##
|
|
5
7
|
# The {LLM::Tracer::Telemetry LLM::Tracer::Telemetry} tracer provides
|
|
@@ -49,6 +51,42 @@ module LLM
|
|
|
49
51
|
setup!
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
##
|
|
55
|
+
# When +trace_group_id+ is provided, it is converted to an OpenTelemetry
|
|
56
|
+
# trace_id (via a deterministic 16-byte hash) so all spans until {#stop_trace}
|
|
57
|
+
# share that trace_id and appear as one trace in OTLP/Langfuse.
|
|
58
|
+
#
|
|
59
|
+
# @param (see LLM::Tracer#start_trace)
|
|
60
|
+
# @return [self]
|
|
61
|
+
def start_trace(trace_group_id: nil, name: "llm", attributes: {})
|
|
62
|
+
return self if trace_group_id.to_s.empty?
|
|
63
|
+
|
|
64
|
+
span_context = span_context_from_trace_group_id(trace_group_id.to_s)
|
|
65
|
+
parent_ctx = ::OpenTelemetry::Trace.context_with_span(
|
|
66
|
+
::OpenTelemetry::Trace.non_recording_span(span_context)
|
|
67
|
+
)
|
|
68
|
+
attrs = attributes.compact
|
|
69
|
+
attrs["llm.trace_group_id"] = trace_group_id.to_s
|
|
70
|
+
root_span = @tracer.start_span(
|
|
71
|
+
name,
|
|
72
|
+
kind: :server,
|
|
73
|
+
attributes: attrs,
|
|
74
|
+
with_parent: parent_ctx
|
|
75
|
+
)
|
|
76
|
+
thread[thread_root_span_key] = root_span
|
|
77
|
+
thread[thread_root_context_key] = ::OpenTelemetry::Trace.context_with_span(root_span)
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# @return [self]
|
|
83
|
+
def stop_trace
|
|
84
|
+
thread[thread_root_span_key]&.finish
|
|
85
|
+
thread[thread_root_span_key] = nil
|
|
86
|
+
thread[thread_root_context_key] = nil
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
52
90
|
##
|
|
53
91
|
# @param (see LLM::Tracer#on_request_start)
|
|
54
92
|
def on_request_start(operation:, model: nil)
|
|
@@ -96,7 +134,7 @@ module LLM
|
|
|
96
134
|
"server.port" => provider_port
|
|
97
135
|
}.compact
|
|
98
136
|
span_name = ["execute_tool", name].compact.join(" ")
|
|
99
|
-
span =
|
|
137
|
+
span = create_span(span_name.empty? ? "gen_ai.tool" : span_name, attributes:)
|
|
100
138
|
span.add_event("gen_ai.tool.start")
|
|
101
139
|
span
|
|
102
140
|
end
|
|
@@ -155,6 +193,49 @@ module LLM
|
|
|
155
193
|
|
|
156
194
|
private
|
|
157
195
|
|
|
196
|
+
##
|
|
197
|
+
# @api private
|
|
198
|
+
def create_span(name, kind: :client, attributes: {})
|
|
199
|
+
root_context = thread[thread_root_context_key]
|
|
200
|
+
opts = {kind:, attributes:}
|
|
201
|
+
opts[:with_parent] = root_context if root_context
|
|
202
|
+
@tracer.start_span(name, **opts)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
##
|
|
206
|
+
# @api private
|
|
207
|
+
def thread_root_span_key
|
|
208
|
+
@thread_root_span_key ||= :"llm.telemetry.root_span.#{object_id}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# @api private
|
|
213
|
+
def thread_root_context_key
|
|
214
|
+
@thread_root_context_key ||= :"llm.telemetry.root_context.#{object_id}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
##
|
|
218
|
+
# @api private
|
|
219
|
+
def thread
|
|
220
|
+
Thread.current
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
# Converts a string trace_group_id to an OpenTelemetry SpanContext so all
|
|
225
|
+
# spans created with this context share the same trace_id.
|
|
226
|
+
# @api private
|
|
227
|
+
def span_context_from_trace_group_id(trace_group_id)
|
|
228
|
+
trace_id = Digest::MD5.digest(trace_group_id)
|
|
229
|
+
trace_id = ::OpenTelemetry::Trace.generate_trace_id if trace_id == ::OpenTelemetry::Trace::INVALID_TRACE_ID
|
|
230
|
+
span_id = Digest::SHA256.digest(trace_group_id)[0, 8]
|
|
231
|
+
span_id = ::OpenTelemetry::Trace.generate_span_id if span_id == ::OpenTelemetry::Trace::INVALID_SPAN_ID
|
|
232
|
+
::OpenTelemetry::Trace::SpanContext.new(
|
|
233
|
+
trace_id:,
|
|
234
|
+
span_id:,
|
|
235
|
+
trace_flags: ::OpenTelemetry::Trace::TraceFlags::SAMPLED
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
158
239
|
##
|
|
159
240
|
# @api private
|
|
160
241
|
def setup!
|
|
@@ -209,7 +290,7 @@ module LLM
|
|
|
209
290
|
"server.port" => provider_port
|
|
210
291
|
}.compact
|
|
211
292
|
span_name = [operation, model].compact.join(" ")
|
|
212
|
-
span =
|
|
293
|
+
span = create_span(span_name.empty? ? "gen_ai.request" : span_name, attributes:)
|
|
213
294
|
span.add_event("gen_ai.request.start")
|
|
214
295
|
span
|
|
215
296
|
end
|
|
@@ -221,7 +302,7 @@ module LLM
|
|
|
221
302
|
"server.address" => provider_host,
|
|
222
303
|
"server.port" => provider_port
|
|
223
304
|
}.compact
|
|
224
|
-
span =
|
|
305
|
+
span = create_span(operation, attributes:)
|
|
225
306
|
span.add_event("gen_ai.request.start")
|
|
226
307
|
span
|
|
227
308
|
end
|
data/lib/llm/tracer.rb
CHANGED
|
@@ -89,6 +89,31 @@ module LLM
|
|
|
89
89
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
##
|
|
93
|
+
# Opens a trace group so subsequent LLM spans share the same OpenTelemetry
|
|
94
|
+
# trace_id (and appear as one trace in backends like Langfuse).
|
|
95
|
+
# When +trace_group_id+ is a string, it is used to derive the trace_id.
|
|
96
|
+
#
|
|
97
|
+
# @param [String, nil] trace_group_id
|
|
98
|
+
# Optional. When present, converted to a 16-byte trace_id so all spans
|
|
99
|
+
# created until {#stop_trace} are grouped in one trace.
|
|
100
|
+
# @param [String] name
|
|
101
|
+
# Name for the root span (e.g. "chatbot.turn").
|
|
102
|
+
# @param [Hash] attributes
|
|
103
|
+
# OpenTelemetry attributes to set on the root span.
|
|
104
|
+
# @return [self]
|
|
105
|
+
def start_trace(trace_group_id: nil, name: "llm", attributes: {})
|
|
106
|
+
self
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Finishes the trace group started by {#start_trace}. Safe to call even if
|
|
111
|
+
# no trace is active.
|
|
112
|
+
# @return [self]
|
|
113
|
+
def stop_trace
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
92
117
|
##
|
|
93
118
|
# @return [String]
|
|
94
119
|
def inspect
|
data/lib/llm/version.rb
CHANGED