llm.rb 4.6.0 → 4.8.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 +40 -37
- data/lib/llm/contract/completion.rb +14 -0
- data/lib/llm/eventstream/parser.rb +0 -5
- data/lib/llm/model.rb +115 -0
- data/lib/llm/provider.rb +50 -23
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/models.rb +1 -1
- data/lib/llm/providers/anthropic/request_adapter.rb +20 -3
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/anthropic/response_adapter/models.rb +13 -0
- data/lib/llm/providers/anthropic/response_adapter.rb +2 -0
- data/lib/llm/providers/anthropic.rb +2 -1
- data/lib/llm/providers/gemini/error_handler.rb +18 -3
- data/lib/llm/providers/gemini/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/gemini/response_adapter/models.rb +4 -6
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/models.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/ollama/response_adapter/models.rb +13 -0
- data/lib/llm/providers/ollama/response_adapter.rb +2 -0
- data/lib/llm/providers/openai/error_handler.rb +18 -3
- data/lib/llm/providers/openai/images.rb +17 -11
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +12 -0
- data/lib/llm/providers/openai/response_adapter/models.rb +13 -0
- data/lib/llm/providers/openai/response_adapter.rb +2 -0
- data/lib/llm/providers/openai/responses.rb +7 -0
- data/lib/llm/providers/openai.rb +9 -2
- data/lib/llm/providers/xai/images.rb +7 -6
- data/lib/llm/schema/enum.rb +16 -0
- data/lib/llm/schema.rb +1 -0
- data/lib/llm/tool/param.rb +1 -1
- data/lib/llm/tool.rb +1 -1
- data/lib/llm/tracer/langsmith.rb +144 -0
- data/lib/llm/tracer/logger.rb +8 -0
- data/lib/llm/tracer/null.rb +8 -0
- data/lib/llm/tracer/telemetry.rb +107 -38
- data/lib/llm/tracer.rb +108 -4
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +1 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 696d0be686c58d66ce0904d0ed0aff879906511060704c61adc945b458ed1f37
|
|
4
|
+
data.tar.gz: 7d03d786ff1fdbaa24e470f87d6e0c3c6b5acfc8e6b73c1cda88fdb9c1a4db58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aeb39e7a8b7a9826be90f3f4739ae0fae40df6bc6c7053e77110ccf1ebe9c5f44316b0edf0e2de3782d0fa93dce110ac8cc0328623056b6c71e55be3abf792c3
|
|
7
|
+
data.tar.gz: c665f150d6c12cbe27541ea743a0fbea4eb107eb5bc93d4e9af480460108430a3057852458f9c5bd1fa5d17ed67633462513ff098c02f0fef43de9dc5ab44224
|
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.8.0-green.svg?" alt="Version"></a>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
## About
|
|
@@ -66,7 +66,7 @@ end
|
|
|
66
66
|
llm = LLM.openai(key: ENV["KEY"])
|
|
67
67
|
ses = LLM::Session.new(llm, schema: Report)
|
|
68
68
|
res = ses.talk("Structure this report: 'Database latency spiked at 10:42 UTC, causing 5% request timeouts for 12 minutes.'")
|
|
69
|
-
pp res.
|
|
69
|
+
pp res.content!
|
|
70
70
|
|
|
71
71
|
##
|
|
72
72
|
# {
|
|
@@ -181,31 +181,41 @@ ses.talk(prompt)
|
|
|
181
181
|
|
|
182
182
|
llm.rb is designed for threaded environments with throughput in mind.
|
|
183
183
|
Locks are used selectively, and localized state is preferred wherever
|
|
184
|
-
possible. Blanket locking across every class
|
|
185
|
-
correctness but it
|
|
184
|
+
possible. Blanket locking across every class could help guarantee
|
|
185
|
+
correctness but it could also add contention, reduce throughput,
|
|
186
186
|
and increase complexity.
|
|
187
187
|
|
|
188
188
|
That's why we decided to optimize for both correctness and throughput
|
|
189
189
|
instead. An important part of that design is guaranteeing that
|
|
190
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
|
|
191
|
+
is safe to share and use across threads. [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html) and
|
|
192
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.
|
|
194
|
-
|
|
195
|
-
and
|
|
193
|
+
stateful objects that should be kept local to a single thread.
|
|
194
|
+
|
|
195
|
+
[LLM::Tracer](https://0x1eef.github.io/x/llm.rb/LLM/Tracer.html) and its
|
|
196
|
+
subclasses are also designed to be thread-local, which means that
|
|
197
|
+
`llm.tracer = ...` only impacts the current thread and must be set
|
|
198
|
+
again in each thread where a tracer is desired. This avoids contention
|
|
199
|
+
on tracer state, keeps tracing isolated per thread, and allows different
|
|
200
|
+
tracers to be used in different threads simultaneously.
|
|
201
|
+
|
|
202
|
+
So the recommended pattern is to keep one session, tracer or agent per
|
|
203
|
+
thread, and share a provider across multiple threads:
|
|
204
|
+
|
|
196
205
|
|
|
197
206
|
```ruby
|
|
198
207
|
#!/usr/bin/env ruby
|
|
199
208
|
require "llm"
|
|
200
209
|
|
|
201
|
-
llm = LLM.openai(key: ENV["KEY"]
|
|
210
|
+
llm = LLM.openai(key: ENV["KEY"]).persist!
|
|
202
211
|
schema = llm.schema.object(answer: llm.schema.integer.required)
|
|
203
212
|
|
|
204
213
|
vals = 10.times.map do |x|
|
|
205
214
|
Thread.new do
|
|
215
|
+
llm.tracer = LLM::Tracer::Logger.new(llm, path: "thread#{x}.log")
|
|
206
216
|
ses = LLM::Session.new(llm, schema:)
|
|
207
217
|
res = ses.talk "#{x} + 5 = ?"
|
|
208
|
-
res.
|
|
218
|
+
res.content!
|
|
209
219
|
end
|
|
210
220
|
end.map(&:value)
|
|
211
221
|
|
|
@@ -335,7 +345,7 @@ and the gem should be installed separately:
|
|
|
335
345
|
#!/usr/bin/env ruby
|
|
336
346
|
require "llm"
|
|
337
347
|
|
|
338
|
-
llm = LLM.openai(key: ENV["KEY"]
|
|
348
|
+
llm = LLM.openai(key: ENV["KEY"]).persist!
|
|
339
349
|
res1 = llm.responses.create "message 1"
|
|
340
350
|
res2 = llm.responses.create "message 2", previous_response_id: res1.response_id
|
|
341
351
|
res3 = llm.responses.create "message 3", previous_response_id: res2.response_id
|
|
@@ -349,6 +359,11 @@ can be used to trace LLM requests. It can be useful for debugging, monitoring,
|
|
|
349
359
|
and observability. The primary use case in mind is integration with tools like
|
|
350
360
|
[LangSmith](https://www.langsmith.com/).
|
|
351
361
|
|
|
362
|
+
It is worth mentioning that tracers are local to a thread, and they
|
|
363
|
+
should be configured per thread. That means that `llm.tracer = LLM::Tracer::Telemetry.new(llm)`
|
|
364
|
+
only impacts the current thread, and it should be repeated in each thread where
|
|
365
|
+
tracing is required.
|
|
366
|
+
|
|
352
367
|
The telemetry implementation uses the [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby)
|
|
353
368
|
and is based on the [gen-ai telemetry spec(s)](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/).
|
|
354
369
|
This feature is optional, disabled by default, and the [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby)
|
|
@@ -406,7 +421,8 @@ The llm.rb library includes simple logging support through its
|
|
|
406
421
|
tracer API, and Ruby's standard library ([ruby/logger](https://github.com/ruby/logger)).
|
|
407
422
|
This feature is optional, disabled by default, and it can be useful for debugging and/or
|
|
408
423
|
monitoring requests to LLM providers. The `path` or `io` options can be used to choose
|
|
409
|
-
where logs are written
|
|
424
|
+
where logs are written, and by default it is set to `$stdout`. Like other tracers,
|
|
425
|
+
the logger tracer is local to a thread:
|
|
410
426
|
|
|
411
427
|
```ruby
|
|
412
428
|
#!/usr/bin/env ruby
|
|
@@ -578,12 +594,13 @@ it has been uploaded. The file (a specialized instance of
|
|
|
578
594
|
```ruby
|
|
579
595
|
#!/usr/bin/env ruby
|
|
580
596
|
require "llm"
|
|
597
|
+
require "pp"
|
|
581
598
|
|
|
582
599
|
llm = LLM.openai(key: ENV["KEY"])
|
|
583
600
|
ses = LLM::Session.new(llm)
|
|
584
601
|
file = llm.files.create(file: "/tmp/llm-book.pdf")
|
|
585
602
|
res = ses.talk ["Tell me about this file", file]
|
|
586
|
-
res.
|
|
603
|
+
pp res.content
|
|
587
604
|
```
|
|
588
605
|
|
|
589
606
|
### Prompts
|
|
@@ -674,68 +691,53 @@ puts res.text # => "Good morning."
|
|
|
674
691
|
Some but not all LLM providers implement image generation capabilities that
|
|
675
692
|
can create new images from a prompt, or edit an existing image with a
|
|
676
693
|
prompt. The following example uses the OpenAI provider to create an
|
|
677
|
-
image of a dog on a rocket to the moon. The image is then
|
|
694
|
+
image of a dog on a rocket to the moon. The image is then written to
|
|
678
695
|
`${HOME}/dogonrocket.png` as the final step:
|
|
679
696
|
|
|
680
697
|
```ruby
|
|
681
698
|
#!/usr/bin/env ruby
|
|
682
699
|
require "llm"
|
|
683
|
-
require "open-uri"
|
|
684
|
-
require "fileutils"
|
|
685
|
-
|
|
686
700
|
llm = LLM.openai(key: ENV["KEY"])
|
|
687
701
|
res = llm.images.create(prompt: "a dog on a rocket to the moon")
|
|
688
|
-
res.
|
|
689
|
-
FileUtils.mv OpenURI.open_uri(url).path,
|
|
690
|
-
File.join(Dir.home, "dogonrocket.png")
|
|
691
|
-
end
|
|
702
|
+
IO.copy_stream res.images[0], File.join(Dir.home, "dogonrocket.png")
|
|
692
703
|
```
|
|
693
704
|
|
|
694
705
|
#### Edit
|
|
695
706
|
|
|
696
707
|
The following example is focused on editing a local image with the aid
|
|
697
708
|
of a prompt. The image (`/tmp/llm-logo.png`) is returned to us with a hat.
|
|
698
|
-
The image is then
|
|
709
|
+
The image is then written to `${HOME}/logo-with-hat.png` as
|
|
699
710
|
the final step:
|
|
700
711
|
|
|
701
712
|
```ruby
|
|
702
713
|
#!/usr/bin/env ruby
|
|
703
714
|
require "llm"
|
|
704
|
-
require "open-uri"
|
|
705
|
-
require "fileutils"
|
|
706
|
-
|
|
707
715
|
llm = LLM.openai(key: ENV["KEY"])
|
|
708
716
|
res = llm.images.edit(
|
|
709
717
|
image: "/tmp/llm-logo.png",
|
|
710
718
|
prompt: "add a hat to the logo",
|
|
711
719
|
)
|
|
712
|
-
res.
|
|
713
|
-
FileUtils.mv OpenURI.open_uri(url).path,
|
|
714
|
-
File.join(Dir.home, "logo-with-hat.png")
|
|
715
|
-
end
|
|
720
|
+
IO.copy_stream res.images[0], File.join(Dir.home, "logo-with-hat.png")
|
|
716
721
|
```
|
|
717
722
|
|
|
718
723
|
#### Variations
|
|
719
724
|
|
|
720
725
|
The following example is focused on creating variations of a local image.
|
|
721
726
|
The image (`/tmp/llm-logo.png`) is returned to us with five different variations.
|
|
722
|
-
The images are then
|
|
727
|
+
The images are then written to `${HOME}/logo-variation0.png`, `${HOME}/logo-variation1.png`
|
|
723
728
|
and so on as the final step:
|
|
724
729
|
|
|
725
730
|
```ruby
|
|
726
731
|
#!/usr/bin/env ruby
|
|
727
732
|
require "llm"
|
|
728
|
-
require "open-uri"
|
|
729
|
-
require "fileutils"
|
|
730
|
-
|
|
731
733
|
llm = LLM.openai(key: ENV["KEY"])
|
|
732
734
|
res = llm.images.create_variation(
|
|
733
735
|
image: "/tmp/llm-logo.png",
|
|
734
736
|
n: 5
|
|
735
737
|
)
|
|
736
|
-
res.
|
|
737
|
-
|
|
738
|
-
|
|
738
|
+
res.images.each.with_index do |image, index|
|
|
739
|
+
IO.copy_stream image,
|
|
740
|
+
File.join(Dir.home, "logo-variation#{index}.png")
|
|
739
741
|
end
|
|
740
742
|
```
|
|
741
743
|
|
|
@@ -776,6 +778,7 @@ release:
|
|
|
776
778
|
```ruby
|
|
777
779
|
#!/usr/bin/env ruby
|
|
778
780
|
require "llm"
|
|
781
|
+
require "pp"
|
|
779
782
|
|
|
780
783
|
##
|
|
781
784
|
# List all models
|
|
@@ -789,7 +792,7 @@ end
|
|
|
789
792
|
model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
|
|
790
793
|
ses = LLM::Session.new(llm, model: model.id)
|
|
791
794
|
res = ses.talk "Hello #{model.id} :)"
|
|
792
|
-
res.
|
|
795
|
+
pp res.content
|
|
793
796
|
```
|
|
794
797
|
|
|
795
798
|
## Install
|
|
@@ -43,6 +43,20 @@ module LLM::Contract
|
|
|
43
43
|
raise NotImplementedError, "#{self.class} does not implement '#{__method__}'"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
##
|
|
47
|
+
# @return [String]
|
|
48
|
+
# Returns the LLM response
|
|
49
|
+
def content
|
|
50
|
+
messages.find(&:assistant?).content
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @return [Hash]
|
|
55
|
+
# Returns the LLM response after parsing it as JSON
|
|
56
|
+
def content!
|
|
57
|
+
LLM.json.load(content)
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
##
|
|
47
61
|
# @return [LLM::Usage]
|
|
48
62
|
# Returns usage information
|
|
@@ -80,11 +80,6 @@ module LLM::EventStream
|
|
|
80
80
|
@cursor = newline + 1
|
|
81
81
|
yield(line)
|
|
82
82
|
end
|
|
83
|
-
if @cursor < @buffer.length
|
|
84
|
-
line = @buffer[@cursor..]
|
|
85
|
-
@cursor = @buffer.length
|
|
86
|
-
yield(line)
|
|
87
|
-
end
|
|
88
83
|
return if @cursor.zero?
|
|
89
84
|
@buffer = @buffer[@cursor..] || +""
|
|
90
85
|
@cursor = 0
|
data/lib/llm/model.rb
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The {LLM::Model LLM::Model} class provides a normalized view of
|
|
5
|
+
# a provider model record returned by the models API.
|
|
6
|
+
class LLM::Model
|
|
7
|
+
##
|
|
8
|
+
# The provider-specific model payload.
|
|
9
|
+
# @return [LLM::Object]
|
|
10
|
+
attr_reader :raw
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param [LLM::Object, Hash] raw
|
|
14
|
+
def initialize(raw)
|
|
15
|
+
@raw = raw
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# Returns a normalized identifier suitable for API calls.
|
|
20
|
+
# @return [String, nil]
|
|
21
|
+
def id
|
|
22
|
+
normalize_id(raw.id || raw.model || raw.name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# Returns a display-friendly model name.
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def name
|
|
29
|
+
raw.display_name || raw.displayName || id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Best-effort predicate for chat support.
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def chat?
|
|
36
|
+
return true if anthropic?
|
|
37
|
+
return [*(raw.supportedGenerationMethods || [])].include?("generateContent") if gemini?
|
|
38
|
+
openai_compatible_chat?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Returns a Hash representation of the normalized model.
|
|
43
|
+
# @return [Hash]
|
|
44
|
+
def to_h
|
|
45
|
+
{id:, name:, chat?: chat?}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# @private
|
|
50
|
+
module Collection
|
|
51
|
+
include ::Enumerable
|
|
52
|
+
|
|
53
|
+
##
|
|
54
|
+
# @yield [model]
|
|
55
|
+
# @yieldparam [LLM::Model] model
|
|
56
|
+
# @return [Enumerator, void]
|
|
57
|
+
def each(&)
|
|
58
|
+
return enum_for(:each) unless block_given?
|
|
59
|
+
models.each(&)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Returns an element, or a slice, or nil.
|
|
64
|
+
# @return [Object, Array<Object>, nil]
|
|
65
|
+
def [](*pos, **kw)
|
|
66
|
+
models[*pos, **kw]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def empty?
|
|
72
|
+
models.empty?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
def size
|
|
78
|
+
models.size
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
##
|
|
82
|
+
# Returns normalized models.
|
|
83
|
+
# @return [Array<LLM::Model>]
|
|
84
|
+
def models
|
|
85
|
+
@models ||= raw_models.map { LLM::Model.new(_1) }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def normalize_id(value)
|
|
92
|
+
value&.sub(%r{\Amodels/}, "")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def anthropic?
|
|
96
|
+
raw.type == "model" && raw.key?(:display_name) && raw.key?(:created_at)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def gemini?
|
|
100
|
+
raw.key?(:supportedGenerationMethods)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def openai_compatible_chat?
|
|
104
|
+
value = [id, raw.name, raw.model].compact.join(" ").downcase
|
|
105
|
+
return false if value.include?("embedding")
|
|
106
|
+
return false if value.include?("moderation")
|
|
107
|
+
return false if value.include?("tts")
|
|
108
|
+
return false if value.include?("transcrib")
|
|
109
|
+
return false if value.include?("image")
|
|
110
|
+
return false if value.include?("whisper")
|
|
111
|
+
return false if value.include?("dall")
|
|
112
|
+
return false if value.include?("omni-moderation")
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -37,7 +37,6 @@ class LLM::Provider
|
|
|
37
37
|
@timeout = timeout
|
|
38
38
|
@ssl = ssl
|
|
39
39
|
@client = persistent ? persistent_client : nil
|
|
40
|
-
@tracer = LLM::Tracer::Null.new(self)
|
|
41
40
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
42
41
|
@headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
|
|
43
42
|
@monitor = Monitor.new
|
|
@@ -48,7 +47,7 @@ class LLM::Provider
|
|
|
48
47
|
# @return [String]
|
|
49
48
|
# @note The secret key is redacted in inspect for security reasons
|
|
50
49
|
def inspect
|
|
51
|
-
"#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect} @tracer=#{
|
|
50
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect} @tracer=#{tracer.inspect}>"
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
##
|
|
@@ -265,27 +264,48 @@ class LLM::Provider
|
|
|
265
264
|
|
|
266
265
|
##
|
|
267
266
|
# @return [LLM::Tracer]
|
|
268
|
-
# Returns
|
|
267
|
+
# Returns a thread-local tracer
|
|
269
268
|
def tracer
|
|
270
|
-
|
|
269
|
+
weakmap[self] || LLM::Tracer::Null.new(self)
|
|
271
270
|
end
|
|
272
271
|
|
|
273
272
|
##
|
|
274
|
-
# Set
|
|
273
|
+
# Set a thread-local tracer
|
|
275
274
|
# @example
|
|
276
275
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
277
|
-
#
|
|
276
|
+
# Thread.new do
|
|
277
|
+
# llm.tracer = LLM::Tracer::Logger.new(llm, path: "/path/to/log/1.txt")
|
|
278
|
+
# end
|
|
279
|
+
# Thread.new do
|
|
280
|
+
# llm.tracer = LLM::Tracer::Logger.new(llm, path: "/path/to/log/2.txt")
|
|
281
|
+
# end
|
|
278
282
|
# # ...
|
|
279
283
|
# @param [LLM::Tracer] tracer
|
|
280
284
|
# A tracer
|
|
281
285
|
# @return [void]
|
|
282
286
|
def tracer=(tracer)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
if tracer.nil?
|
|
288
|
+
if weakmap.respond_to?(:delete)
|
|
289
|
+
weakmap.delete(self)
|
|
286
290
|
else
|
|
287
|
-
|
|
291
|
+
weakmap[self] = nil
|
|
288
292
|
end
|
|
293
|
+
else
|
|
294
|
+
weakmap[self] = tracer
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
##
|
|
299
|
+
# This method configures a provider to use a persistent connection pool
|
|
300
|
+
# via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
|
|
301
|
+
# @example
|
|
302
|
+
# llm = LLM.openai(key: ENV["KEY"]).persist!
|
|
303
|
+
# # do something with 'llm'
|
|
304
|
+
# @return [LLM::Provider]
|
|
305
|
+
def persist!
|
|
306
|
+
client = persistent_client
|
|
307
|
+
lock do
|
|
308
|
+
tap { @client = client }
|
|
289
309
|
end
|
|
290
310
|
end
|
|
291
311
|
|
|
@@ -340,9 +360,9 @@ class LLM::Provider
|
|
|
340
360
|
# @raise [SystemCallError]
|
|
341
361
|
# When there is a network error at the operating system level
|
|
342
362
|
# @return [Net::HTTPResponse]
|
|
343
|
-
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
|
|
344
|
-
tracer =
|
|
345
|
-
span = tracer.on_request_start(operation:, model:)
|
|
363
|
+
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, inputs: nil, &b)
|
|
364
|
+
tracer = self.tracer
|
|
365
|
+
span = tracer.on_request_start(operation:, model:, inputs:)
|
|
346
366
|
http = client || transient_client
|
|
347
367
|
args = (Net::HTTP === http) ? [request] : [URI.join(base_uri, request.path), request]
|
|
348
368
|
res = if stream
|
|
@@ -351,11 +371,12 @@ class LLM::Provider
|
|
|
351
371
|
parser = LLM::EventStream::Parser.new
|
|
352
372
|
parser.register(handler)
|
|
353
373
|
res.read_body(parser)
|
|
354
|
-
# If the handler body is empty,
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
|
|
374
|
+
# If the handler body is empty, the response was
|
|
375
|
+
# most likely not streamed or parsing failed.
|
|
376
|
+
# Preserve the raw body in that case so standard
|
|
377
|
+
# JSON/error handling can parse it later.
|
|
378
|
+
body = handler.body.empty? ? parser.body : handler.body
|
|
379
|
+
res.body = Hash === body || Array === body ? LLM::Object.from(body) : body
|
|
359
380
|
ensure
|
|
360
381
|
parser&.free
|
|
361
382
|
end
|
|
@@ -423,14 +444,20 @@ class LLM::Provider
|
|
|
423
444
|
end
|
|
424
445
|
|
|
425
446
|
##
|
|
426
|
-
# @
|
|
427
|
-
def
|
|
428
|
-
|
|
447
|
+
# @api private
|
|
448
|
+
def lock(&)
|
|
449
|
+
@monitor.synchronize(&)
|
|
429
450
|
end
|
|
430
451
|
|
|
431
452
|
##
|
|
432
453
|
# @api private
|
|
433
|
-
def
|
|
434
|
-
|
|
454
|
+
def thread
|
|
455
|
+
Thread.current
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
##
|
|
459
|
+
# @api private
|
|
460
|
+
def weakmap
|
|
461
|
+
thread[:"llm.provider.weakmap"] ||= ObjectSpace::WeakMap.new
|
|
435
462
|
end
|
|
436
463
|
end
|
|
@@ -41,7 +41,7 @@ class LLM::Anthropic
|
|
|
41
41
|
query = URI.encode_www_form(params)
|
|
42
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
|
43
43
|
res, span, tracer = execute(request: req, operation: "request")
|
|
44
|
-
res = ResponseAdapter.adapt(res, type: :
|
|
44
|
+
res = ResponseAdapter.adapt(res, type: :models)
|
|
45
45
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
46
46
|
res
|
|
47
47
|
end
|
|
@@ -9,11 +9,20 @@ class LLM::Anthropic
|
|
|
9
9
|
##
|
|
10
10
|
# @param [Array<LLM::Message>] messages
|
|
11
11
|
# The messages to adapt
|
|
12
|
-
# @return [
|
|
12
|
+
# @return [Hash]
|
|
13
13
|
def adapt(messages, mode: nil)
|
|
14
|
-
messages
|
|
15
|
-
|
|
14
|
+
payload = {messages: [], system: []}
|
|
15
|
+
messages.each do |message|
|
|
16
|
+
adapted = Completion.new(message).adapt
|
|
17
|
+
next if adapted.nil?
|
|
18
|
+
if system?(message)
|
|
19
|
+
payload[:system].concat Array(adapted[:content])
|
|
20
|
+
else
|
|
21
|
+
payload[:messages] << adapted
|
|
22
|
+
end
|
|
16
23
|
end
|
|
24
|
+
payload.delete(:system) if payload[:system].empty?
|
|
25
|
+
payload
|
|
17
26
|
end
|
|
18
27
|
|
|
19
28
|
private
|
|
@@ -25,5 +34,13 @@ class LLM::Anthropic
|
|
|
25
34
|
return {} unless tools&.any?
|
|
26
35
|
{tools: tools.map { _1.respond_to?(:adapt) ? _1.adapt(self) : _1 }}
|
|
27
36
|
end
|
|
37
|
+
|
|
38
|
+
def system?(message)
|
|
39
|
+
if message.respond_to?(:system?)
|
|
40
|
+
message.system?
|
|
41
|
+
else
|
|
42
|
+
Hash === message and message[:role].to_s == "system"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
28
45
|
end
|
|
29
46
|
end
|
|
@@ -45,6 +45,18 @@ module LLM::Anthropic::ResponseAdapter
|
|
|
45
45
|
body.model
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
##
|
|
49
|
+
# (see LLM::Contract::Completion#content)
|
|
50
|
+
def content
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# (see LLM::Contract::Completion#content!)
|
|
56
|
+
def content!
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
private
|
|
49
61
|
|
|
50
62
|
def adapt_choices
|
|
@@ -7,6 +7,7 @@ class LLM::Anthropic
|
|
|
7
7
|
require_relative "response_adapter/completion"
|
|
8
8
|
require_relative "response_adapter/enumerable"
|
|
9
9
|
require_relative "response_adapter/file"
|
|
10
|
+
require_relative "response_adapter/models"
|
|
10
11
|
require_relative "response_adapter/web_search"
|
|
11
12
|
|
|
12
13
|
module_function
|
|
@@ -27,6 +28,7 @@ class LLM::Anthropic
|
|
|
27
28
|
when :completion then LLM::Anthropic::ResponseAdapter::Completion
|
|
28
29
|
when :enumerable then LLM::Anthropic::ResponseAdapter::Enumerable
|
|
29
30
|
when :file then LLM::Anthropic::ResponseAdapter::File
|
|
31
|
+
when :models then LLM::Anthropic::ResponseAdapter::Models
|
|
30
32
|
when :web_search then LLM::Anthropic::ResponseAdapter::WebSearch
|
|
31
33
|
else
|
|
32
34
|
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
@@ -140,7 +140,8 @@ module LLM
|
|
|
140
140
|
|
|
141
141
|
def build_complete_request(prompt, params, role)
|
|
142
142
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
143
|
-
|
|
143
|
+
payload = adapt(messages)
|
|
144
|
+
body = LLM.json.dump(payload.merge!(params))
|
|
144
145
|
req = Net::HTTP::Post.new("/v1/messages", headers)
|
|
145
146
|
set_body_stream(req, StringIO.new(body))
|
|
146
147
|
req
|
|
@@ -35,15 +35,15 @@ class LLM::Gemini
|
|
|
35
35
|
ex = error
|
|
36
36
|
@tracer.on_request_error(ex:, span:)
|
|
37
37
|
ensure
|
|
38
|
-
raise(ex)
|
|
38
|
+
raise(ex) if ex
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
43
|
##
|
|
44
|
-
# @return [LLM::Object]
|
|
44
|
+
# @return [String, LLM::Object]
|
|
45
45
|
def body
|
|
46
|
-
@body ||=
|
|
46
|
+
@body ||= parse_body!
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
##
|
|
@@ -65,5 +65,20 @@ class LLM::Gemini
|
|
|
65
65
|
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Tries to parse the response body as a LLM::Object
|
|
71
|
+
# @return [String, LLM::Object]
|
|
72
|
+
def parse_body!
|
|
73
|
+
if String === res.body
|
|
74
|
+
LLM::Object.from LLM.json.load(res.body)
|
|
75
|
+
elsif Hash === res.body
|
|
76
|
+
LLM::Object.from(res.body)
|
|
77
|
+
else
|
|
78
|
+
res.body
|
|
79
|
+
end
|
|
80
|
+
rescue
|
|
81
|
+
res.body
|
|
82
|
+
end
|
|
68
83
|
end
|
|
69
84
|
end
|
|
@@ -45,6 +45,18 @@ module LLM::Gemini::ResponseAdapter
|
|
|
45
45
|
body.modelVersion
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
##
|
|
49
|
+
# (see LLM::Contract::Completion#content)
|
|
50
|
+
def content
|
|
51
|
+
super
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# (see LLM::Contract::Completion#content!)
|
|
56
|
+
def content!
|
|
57
|
+
super
|
|
58
|
+
end
|
|
59
|
+
|
|
48
60
|
private
|
|
49
61
|
|
|
50
62
|
def adapt_choices
|