llm.rb 4.7.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 +32 -31
- data/lib/llm/eventstream/parser.rb +0 -5
- data/lib/llm/model.rb +115 -0
- data/lib/llm/provider.rb +36 -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/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/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/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/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 +91 -71
- 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
|
|
@@ -181,18 +181,27 @@ 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
|
|
@@ -203,6 +212,7 @@ 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
218
|
res.content!
|
|
@@ -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
|
|
@@ -675,68 +691,53 @@ puts res.text # => "Good morning."
|
|
|
675
691
|
Some but not all LLM providers implement image generation capabilities that
|
|
676
692
|
can create new images from a prompt, or edit an existing image with a
|
|
677
693
|
prompt. The following example uses the OpenAI provider to create an
|
|
678
|
-
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
|
|
679
695
|
`${HOME}/dogonrocket.png` as the final step:
|
|
680
696
|
|
|
681
697
|
```ruby
|
|
682
698
|
#!/usr/bin/env ruby
|
|
683
699
|
require "llm"
|
|
684
|
-
require "open-uri"
|
|
685
|
-
require "fileutils"
|
|
686
|
-
|
|
687
700
|
llm = LLM.openai(key: ENV["KEY"])
|
|
688
701
|
res = llm.images.create(prompt: "a dog on a rocket to the moon")
|
|
689
|
-
res.
|
|
690
|
-
FileUtils.mv OpenURI.open_uri(url).path,
|
|
691
|
-
File.join(Dir.home, "dogonrocket.png")
|
|
692
|
-
end
|
|
702
|
+
IO.copy_stream res.images[0], File.join(Dir.home, "dogonrocket.png")
|
|
693
703
|
```
|
|
694
704
|
|
|
695
705
|
#### Edit
|
|
696
706
|
|
|
697
707
|
The following example is focused on editing a local image with the aid
|
|
698
708
|
of a prompt. The image (`/tmp/llm-logo.png`) is returned to us with a hat.
|
|
699
|
-
The image is then
|
|
709
|
+
The image is then written to `${HOME}/logo-with-hat.png` as
|
|
700
710
|
the final step:
|
|
701
711
|
|
|
702
712
|
```ruby
|
|
703
713
|
#!/usr/bin/env ruby
|
|
704
714
|
require "llm"
|
|
705
|
-
require "open-uri"
|
|
706
|
-
require "fileutils"
|
|
707
|
-
|
|
708
715
|
llm = LLM.openai(key: ENV["KEY"])
|
|
709
716
|
res = llm.images.edit(
|
|
710
717
|
image: "/tmp/llm-logo.png",
|
|
711
718
|
prompt: "add a hat to the logo",
|
|
712
719
|
)
|
|
713
|
-
res.
|
|
714
|
-
FileUtils.mv OpenURI.open_uri(url).path,
|
|
715
|
-
File.join(Dir.home, "logo-with-hat.png")
|
|
716
|
-
end
|
|
720
|
+
IO.copy_stream res.images[0], File.join(Dir.home, "logo-with-hat.png")
|
|
717
721
|
```
|
|
718
722
|
|
|
719
723
|
#### Variations
|
|
720
724
|
|
|
721
725
|
The following example is focused on creating variations of a local image.
|
|
722
726
|
The image (`/tmp/llm-logo.png`) is returned to us with five different variations.
|
|
723
|
-
The images are then
|
|
727
|
+
The images are then written to `${HOME}/logo-variation0.png`, `${HOME}/logo-variation1.png`
|
|
724
728
|
and so on as the final step:
|
|
725
729
|
|
|
726
730
|
```ruby
|
|
727
731
|
#!/usr/bin/env ruby
|
|
728
732
|
require "llm"
|
|
729
|
-
require "open-uri"
|
|
730
|
-
require "fileutils"
|
|
731
|
-
|
|
732
733
|
llm = LLM.openai(key: ENV["KEY"])
|
|
733
734
|
res = llm.images.create_variation(
|
|
734
735
|
image: "/tmp/llm-logo.png",
|
|
735
736
|
n: 5
|
|
736
737
|
)
|
|
737
|
-
res.
|
|
738
|
-
|
|
739
|
-
|
|
738
|
+
res.images.each.with_index do |image, index|
|
|
739
|
+
IO.copy_stream image,
|
|
740
|
+
File.join(Dir.home, "logo-variation#{index}.png")
|
|
740
741
|
end
|
|
741
742
|
```
|
|
742
743
|
|
|
@@ -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,34 @@ 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
|
|
289
295
|
end
|
|
290
296
|
end
|
|
291
297
|
|
|
@@ -354,9 +360,9 @@ class LLM::Provider
|
|
|
354
360
|
# @raise [SystemCallError]
|
|
355
361
|
# When there is a network error at the operating system level
|
|
356
362
|
# @return [Net::HTTPResponse]
|
|
357
|
-
def execute(request:, operation:, stream: nil, stream_parser: self.stream_parser, model: nil, &b)
|
|
358
|
-
tracer =
|
|
359
|
-
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:)
|
|
360
366
|
http = client || transient_client
|
|
361
367
|
args = (Net::HTTP === http) ? [request] : [URI.join(base_uri, request.path), request]
|
|
362
368
|
res = if stream
|
|
@@ -365,11 +371,12 @@ class LLM::Provider
|
|
|
365
371
|
parser = LLM::EventStream::Parser.new
|
|
366
372
|
parser.register(handler)
|
|
367
373
|
res.read_body(parser)
|
|
368
|
-
# If the handler body is empty,
|
|
369
|
-
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
|
|
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
|
|
373
380
|
ensure
|
|
374
381
|
parser&.free
|
|
375
382
|
end
|
|
@@ -437,14 +444,20 @@ class LLM::Provider
|
|
|
437
444
|
end
|
|
438
445
|
|
|
439
446
|
##
|
|
440
|
-
# @
|
|
441
|
-
def
|
|
442
|
-
|
|
447
|
+
# @api private
|
|
448
|
+
def lock(&)
|
|
449
|
+
@monitor.synchronize(&)
|
|
443
450
|
end
|
|
444
451
|
|
|
445
452
|
##
|
|
446
453
|
# @api private
|
|
447
|
-
def
|
|
448
|
-
|
|
454
|
+
def thread
|
|
455
|
+
Thread.current
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
##
|
|
459
|
+
# @api private
|
|
460
|
+
def weakmap
|
|
461
|
+
thread[:"llm.provider.weakmap"] ||= ObjectSpace::WeakMap.new
|
|
449
462
|
end
|
|
450
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
|
|
@@ -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
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM::Gemini::ResponseAdapter
|
|
4
4
|
module Models
|
|
5
|
-
include ::
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
models.each { yield(_1) }
|
|
9
|
-
end
|
|
5
|
+
include LLM::Model::Collection
|
|
6
|
+
|
|
7
|
+
private
|
|
10
8
|
|
|
11
|
-
def
|
|
9
|
+
def raw_models
|
|
12
10
|
body.models || []
|
|
13
11
|
end
|
|
14
12
|
end
|
|
@@ -44,7 +44,7 @@ class LLM::Ollama
|
|
|
44
44
|
query = URI.encode_www_form(params)
|
|
45
45
|
req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
|
|
46
46
|
res, span, tracer = execute(request: req, operation: "request")
|
|
47
|
-
res =
|
|
47
|
+
res = ResponseAdapter.adapt(res, type: :models)
|
|
48
48
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
49
49
|
res
|
|
50
50
|
end
|
|
@@ -6,6 +6,7 @@ class LLM::Ollama
|
|
|
6
6
|
module ResponseAdapter
|
|
7
7
|
require_relative "response_adapter/completion"
|
|
8
8
|
require_relative "response_adapter/embedding"
|
|
9
|
+
require_relative "response_adapter/models"
|
|
9
10
|
|
|
10
11
|
module_function
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ class LLM::Ollama
|
|
|
24
25
|
case type
|
|
25
26
|
when :completion then LLM::Ollama::ResponseAdapter::Completion
|
|
26
27
|
when :embedding then LLM::Ollama::ResponseAdapter::Embedding
|
|
28
|
+
when :models then LLM::Ollama::ResponseAdapter::Models
|
|
27
29
|
else
|
|
28
30
|
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
29
31
|
end
|
|
@@ -35,15 +35,15 @@ class LLM::OpenAI
|
|
|
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
|
##
|
|
@@ -79,5 +79,20 @@ class LLM::OpenAI
|
|
|
79
79
|
LLM::InvalidRequestError.new(error["message"]).tap { _1.response = res }
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Tries to parse the response body as a LLM::Object
|
|
85
|
+
# @return [String, LLM::Object]
|
|
86
|
+
def parse_body!
|
|
87
|
+
if String === res.body
|
|
88
|
+
LLM::Object.from LLM.json.load(res.body)
|
|
89
|
+
elsif Hash === res.body
|
|
90
|
+
LLM::Object.from(res.body)
|
|
91
|
+
else
|
|
92
|
+
res.body
|
|
93
|
+
end
|
|
94
|
+
rescue
|
|
95
|
+
res.body
|
|
96
|
+
end
|
|
82
97
|
end
|
|
83
98
|
end
|