llm.rb 2.1.0 → 3.0.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 +6 -0
- data/lib/llm/bot.rb +4 -4
- data/lib/llm/buffer.rb +0 -9
- data/lib/llm/contract/completion.rb +57 -0
- data/lib/llm/contract.rb +48 -0
- data/lib/llm/error.rb +22 -14
- data/lib/llm/eventhandler.rb +6 -4
- data/lib/llm/eventstream/parser.rb +18 -13
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +109 -0
- data/lib/llm/message.rb +7 -28
- data/lib/llm/multipart/enumerator_io.rb +86 -0
- data/lib/llm/multipart.rb +32 -51
- data/lib/llm/object/builder.rb +6 -6
- data/lib/llm/object/kernel.rb +2 -2
- data/lib/llm/object.rb +23 -8
- data/lib/llm/provider.rb +11 -3
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/files.rb +4 -5
- data/lib/llm/providers/anthropic/models.rb +1 -2
- data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +66 -0
- data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +6 -6
- data/lib/llm/providers/anthropic.rb +8 -11
- data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
- data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/deepseek.rb +2 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/error_handler.rb +3 -3
- data/lib/llm/providers/gemini/files.rb +4 -7
- data/lib/llm/providers/gemini/images.rb +9 -14
- data/lib/llm/providers/gemini/models.rb +1 -2
- data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
- data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
- data/lib/llm/providers/gemini/response_adapter/completion.rb +67 -0
- data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
- data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/gemini/response_adapter.rb +42 -0
- data/lib/llm/providers/gemini/stream_parser.rb +37 -32
- data/lib/llm/providers/gemini.rb +10 -14
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/ollama/response_adapter/completion.rb +61 -0
- data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter.rb +32 -0
- data/lib/llm/providers/ollama/stream_parser.rb +2 -2
- data/lib/llm/providers/ollama.rb +8 -10
- data/lib/llm/providers/openai/audio.rb +1 -1
- data/lib/llm/providers/openai/error_handler.rb +12 -2
- data/lib/llm/providers/openai/files.rb +3 -6
- data/lib/llm/providers/openai/images.rb +4 -5
- data/lib/llm/providers/openai/models.rb +1 -3
- data/lib/llm/providers/openai/moderations.rb +3 -5
- data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
- data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
- data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
- data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
- data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +62 -0
- data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
- data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/openai/response_adapter.rb +47 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +6 -5
- data/lib/llm/providers/openai/vector_stores.rb +8 -9
- data/lib/llm/providers/openai.rb +12 -14
- data/lib/llm/response.rb +2 -5
- data/lib/llm/usage.rb +10 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +33 -1
- metadata +44 -35
- data/lib/llm/providers/anthropic/response/completion.rb +0 -39
- data/lib/llm/providers/gemini/response/completion.rb +0 -35
- data/lib/llm/providers/ollama/response/completion.rb +0 -28
- data/lib/llm/providers/openai/response/completion.rb +0 -40
data/lib/llm/multipart.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# @private
|
|
6
6
|
class LLM::Multipart
|
|
7
7
|
require "securerandom"
|
|
8
|
+
require_relative "multipart/enumerator_io"
|
|
9
|
+
CHUNK_SIZE = 16 * 1024
|
|
8
10
|
|
|
9
11
|
##
|
|
10
12
|
# @return [String]
|
|
@@ -27,69 +29,36 @@ class LLM::Multipart
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
##
|
|
30
|
-
# Returns the multipart request body
|
|
31
|
-
# @return [
|
|
32
|
+
# Returns the multipart request body as a stream
|
|
33
|
+
# @return [LLM::Multipart::EnumeratorIO]
|
|
32
34
|
def body
|
|
33
|
-
|
|
34
|
-
[*parts, StringIO.new("--#{@boundary}--\r\n".b)].each { IO.copy_stream(_1.tap(&:rewind), io) }
|
|
35
|
-
io.tap(&:rewind)
|
|
35
|
+
LLM::Multipart::EnumeratorIO.new(enum_for(:each_part))
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
39
39
|
|
|
40
40
|
attr_reader :params
|
|
41
41
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
build_file(locals) do |body|
|
|
45
|
-
IO.copy_stream(file.path, body)
|
|
46
|
-
body << "\r\n"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def form(locals, value)
|
|
51
|
-
locals = locals.merge(value:)
|
|
52
|
-
build_form(locals) do |body|
|
|
53
|
-
body << value.to_s
|
|
54
|
-
body << "\r\n"
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def build_file(locals)
|
|
59
|
-
StringIO.new("".b).tap do |io|
|
|
60
|
-
io << "--#{locals[:boundary]}" \
|
|
61
|
-
"\r\n" \
|
|
62
|
-
"Content-Disposition: form-data; name=\"#{locals[:key]}\";" \
|
|
63
|
-
"filename=\"#{locals[:filename]}\"" \
|
|
64
|
-
"\r\n" \
|
|
65
|
-
"Content-Type: #{locals[:content_type]}" \
|
|
66
|
-
"\r\n\r\n"
|
|
67
|
-
yield(io)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def build_form(locals)
|
|
72
|
-
StringIO.new("".b).tap do |io|
|
|
73
|
-
io << "--#{locals[:boundary]}" \
|
|
74
|
-
"\r\n" \
|
|
75
|
-
"Content-Disposition: form-data; name=\"#{locals[:key]}\"" \
|
|
76
|
-
"\r\n\r\n"
|
|
77
|
-
yield(io)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
##
|
|
82
|
-
# Returns the multipart request body parts
|
|
83
|
-
# @return [Array<String>]
|
|
84
|
-
def parts
|
|
85
|
-
params.map do |key, value|
|
|
42
|
+
def each_part
|
|
43
|
+
params.each do |key, value|
|
|
86
44
|
locals = {key: key.to_s.b, boundary: boundary.to_s.b}
|
|
87
45
|
if value.respond_to?(:path)
|
|
88
|
-
|
|
46
|
+
locals = locals.merge(attributes(value))
|
|
47
|
+
yield file_header(locals)
|
|
48
|
+
File.open(value.path, "rb") do |io|
|
|
49
|
+
while (chunk = io.read(CHUNK_SIZE))
|
|
50
|
+
yield chunk
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
yield "\r\n".b
|
|
89
54
|
else
|
|
90
|
-
|
|
55
|
+
locals = locals.merge(value:)
|
|
56
|
+
yield form_header(locals)
|
|
57
|
+
yield value.to_s.b
|
|
58
|
+
yield "\r\n".b
|
|
91
59
|
end
|
|
92
60
|
end
|
|
61
|
+
yield "--#{@boundary}--\r\n".b
|
|
93
62
|
end
|
|
94
63
|
|
|
95
64
|
def attributes(file)
|
|
@@ -98,4 +67,16 @@ class LLM::Multipart
|
|
|
98
67
|
content_type: LLM::Mime[file].b
|
|
99
68
|
}
|
|
100
69
|
end
|
|
70
|
+
|
|
71
|
+
def file_header(locals)
|
|
72
|
+
"--#{locals[:boundary]}\r\n" \
|
|
73
|
+
"Content-Disposition: form-data; name=\"#{locals[:key]}\";" \
|
|
74
|
+
"filename=\"#{locals[:filename]}\"\r\n" \
|
|
75
|
+
"Content-Type: #{locals[:content_type]}\r\n\r\n"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def form_header(locals)
|
|
79
|
+
"--#{locals[:boundary]}\r\n" \
|
|
80
|
+
"Content-Disposition: form-data; name=\"#{locals[:key]}\"\r\n\r\n"
|
|
81
|
+
end
|
|
101
82
|
end
|
data/lib/llm/object/builder.rb
CHANGED
|
@@ -6,17 +6,17 @@ class LLM::Object
|
|
|
6
6
|
module Builder
|
|
7
7
|
##
|
|
8
8
|
# @example
|
|
9
|
-
# obj = LLM::Object.
|
|
9
|
+
# obj = LLM::Object.from(person: {name: 'John'})
|
|
10
10
|
# obj.person.name # => 'John'
|
|
11
11
|
# obj.person.class # => LLM::Object
|
|
12
12
|
# @param [Hash, LLM::Object, Array] obj
|
|
13
13
|
# A Hash object
|
|
14
14
|
# @return [LLM::Object]
|
|
15
15
|
# An LLM::Object object initialized by visiting `obj` with recursion
|
|
16
|
-
def
|
|
16
|
+
def from(obj)
|
|
17
17
|
case obj
|
|
18
|
-
when self then
|
|
19
|
-
when Array then obj.map { |v|
|
|
18
|
+
when self then from(obj.to_h)
|
|
19
|
+
when Array then obj.map { |v| from(v) }
|
|
20
20
|
else
|
|
21
21
|
visited = {}
|
|
22
22
|
obj.each { visited[_1] = visit(_2) }
|
|
@@ -28,8 +28,8 @@ class LLM::Object
|
|
|
28
28
|
|
|
29
29
|
def visit(value)
|
|
30
30
|
case value
|
|
31
|
-
when self then
|
|
32
|
-
when Hash then
|
|
31
|
+
when self then from(value.to_h)
|
|
32
|
+
when Hash then from(value)
|
|
33
33
|
when Array then value.map { |v| visit(v) }
|
|
34
34
|
else value
|
|
35
35
|
end
|
data/lib/llm/object/kernel.rb
CHANGED
|
@@ -26,11 +26,11 @@ class LLM::Object
|
|
|
26
26
|
alias_method :is_a?, :kind_of?
|
|
27
27
|
|
|
28
28
|
def respond_to?(m, include_private = false)
|
|
29
|
-
|
|
29
|
+
!!key(m) || self.class.method_defined?(m) || super
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def respond_to_missing?(m, include_private = false)
|
|
33
|
-
|
|
33
|
+
!!key(m) || super
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def object_id
|
data/lib/llm/object.rb
CHANGED
|
@@ -17,7 +17,7 @@ class LLM::Object < BasicObject
|
|
|
17
17
|
# @param [Hash] h
|
|
18
18
|
# @return [LLM::Object]
|
|
19
19
|
def initialize(h = {})
|
|
20
|
-
@h = h
|
|
20
|
+
@h = h || {}
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
##
|
|
@@ -33,7 +33,7 @@ class LLM::Object < BasicObject
|
|
|
33
33
|
# @param [Symbol, #to_sym] k
|
|
34
34
|
# @return [Object]
|
|
35
35
|
def [](k)
|
|
36
|
-
@h[k
|
|
36
|
+
@h[key(k)]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
##
|
|
@@ -41,7 +41,7 @@ class LLM::Object < BasicObject
|
|
|
41
41
|
# @param [Object] v
|
|
42
42
|
# @return [void]
|
|
43
43
|
def []=(k, v)
|
|
44
|
-
@h[k.
|
|
44
|
+
@h[k.to_s] = v
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
##
|
|
@@ -59,9 +59,14 @@ class LLM::Object < BasicObject
|
|
|
59
59
|
##
|
|
60
60
|
# @return [Hash]
|
|
61
61
|
def to_h
|
|
62
|
-
@h
|
|
62
|
+
@h.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# @return [Hash]
|
|
67
|
+
def to_hash
|
|
68
|
+
@h.transform_keys(&:to_sym)
|
|
63
69
|
end
|
|
64
|
-
alias_method :to_hash, :to_h
|
|
65
70
|
|
|
66
71
|
##
|
|
67
72
|
# @return [Object, nil]
|
|
@@ -79,9 +84,19 @@ class LLM::Object < BasicObject
|
|
|
79
84
|
|
|
80
85
|
def method_missing(m, *args, &b)
|
|
81
86
|
if m.to_s.end_with?("=")
|
|
82
|
-
|
|
83
|
-
elsif
|
|
84
|
-
@h[
|
|
87
|
+
self[m[0..-2]] = args.first
|
|
88
|
+
elsif k = key(m)
|
|
89
|
+
@h[k]
|
|
90
|
+
else
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def key(k)
|
|
96
|
+
if @h.key?(k.to_s)
|
|
97
|
+
k.to_s
|
|
98
|
+
elsif @h.key?(k.to_sym)
|
|
99
|
+
k.to_sym
|
|
85
100
|
else
|
|
86
101
|
nil
|
|
87
102
|
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -280,7 +280,7 @@ class LLM::Provider
|
|
|
280
280
|
# When authentication fails
|
|
281
281
|
# @raise [LLM::Error::RateLimit]
|
|
282
282
|
# When the rate limit is exceeded
|
|
283
|
-
# @raise [LLM::Error
|
|
283
|
+
# @raise [LLM::Error]
|
|
284
284
|
# When any other unsuccessful status code is returned
|
|
285
285
|
# @raise [SystemCallError]
|
|
286
286
|
# When there is a network error at the operating system level
|
|
@@ -297,7 +297,7 @@ class LLM::Provider
|
|
|
297
297
|
# response was most likely not streamed or
|
|
298
298
|
# parsing has failed. In that case, we fallback
|
|
299
299
|
# on the original response body.
|
|
300
|
-
res.body = handler.body.empty? ? parser.body
|
|
300
|
+
res.body = LLM::Object.from(handler.body.empty? ? parser.body : handler.body)
|
|
301
301
|
ensure
|
|
302
302
|
parser&.free
|
|
303
303
|
end
|
|
@@ -315,9 +315,17 @@ class LLM::Provider
|
|
|
315
315
|
# @return [Net::HTTPResponse]
|
|
316
316
|
def handle_response(res)
|
|
317
317
|
case res
|
|
318
|
-
when Net::HTTPOK then res
|
|
318
|
+
when Net::HTTPOK then res.body = parse_response(res)
|
|
319
319
|
else error_handler.new(res).raise_error!
|
|
320
320
|
end
|
|
321
|
+
res
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def parse_response(res)
|
|
325
|
+
case res["content-type"]
|
|
326
|
+
when %r|\Aapplication/json\s*| then LLM::Object.from(LLM.json.load(res.body))
|
|
327
|
+
else res.body
|
|
328
|
+
end
|
|
321
329
|
end
|
|
322
330
|
|
|
323
331
|
##
|
|
@@ -29,7 +29,7 @@ class LLM::Anthropic
|
|
|
29
29
|
when Net::HTTPTooManyRequests
|
|
30
30
|
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
|
31
31
|
else
|
|
32
|
-
raise LLM::
|
|
32
|
+
raise LLM::Error.new { _1.response = res }, "Unexpected response"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -15,7 +15,6 @@ class LLM::Anthropic
|
|
|
15
15
|
# bot.chat ["Tell me about this PDF", file]
|
|
16
16
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
17
17
|
class Files
|
|
18
|
-
require_relative "response/file"
|
|
19
18
|
##
|
|
20
19
|
# Returns a new Files object
|
|
21
20
|
# @param provider [LLM::Provider]
|
|
@@ -40,7 +39,7 @@ class LLM::Anthropic
|
|
|
40
39
|
query = URI.encode_www_form(params)
|
|
41
40
|
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
|
42
41
|
res = execute(request: req)
|
|
43
|
-
|
|
42
|
+
ResponseAdapter.adapt(res, type: :enumerable)
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
##
|
|
@@ -59,7 +58,7 @@ class LLM::Anthropic
|
|
|
59
58
|
req["content-type"] = multi.content_type
|
|
60
59
|
set_body_stream(req, multi.body)
|
|
61
60
|
res = execute(request: req)
|
|
62
|
-
|
|
61
|
+
ResponseAdapter.adapt(res, type: :file)
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
##
|
|
@@ -78,7 +77,7 @@ class LLM::Anthropic
|
|
|
78
77
|
query = URI.encode_www_form(params)
|
|
79
78
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
80
79
|
res = execute(request: req)
|
|
81
|
-
|
|
80
|
+
ResponseAdapter.adapt(res, type: :file)
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
##
|
|
@@ -97,7 +96,7 @@ class LLM::Anthropic
|
|
|
97
96
|
file_id = file.respond_to?(:id) ? file.id : file
|
|
98
97
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
99
98
|
res = execute(request: req)
|
|
100
|
-
|
|
99
|
+
ResponseAdapter.adapt(res, type: :file)
|
|
101
100
|
end
|
|
102
101
|
alias_method :retrieve_metadata, :get_metadata
|
|
103
102
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class LLM::Anthropic
|
|
4
|
-
require_relative "response/enumerable"
|
|
5
4
|
##
|
|
6
5
|
# The {LLM::Anthropic::Models LLM::Anthropic::Models} class provides a model
|
|
7
6
|
# object for interacting with [Anthropic's models API](https://platform.anthropic.com/docs/api-reference/models/list).
|
|
@@ -42,7 +41,7 @@ class LLM::Anthropic
|
|
|
42
41
|
query = URI.encode_www_form(params)
|
|
43
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
|
44
43
|
res = execute(request: req)
|
|
45
|
-
|
|
44
|
+
ResponseAdapter.adapt(res, type: :enumerable)
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
private
|
data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::Anthropic::
|
|
3
|
+
module LLM::Anthropic::RequestAdapter
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
class
|
|
6
|
+
class Completion
|
|
7
7
|
##
|
|
8
8
|
# @param [LLM::Message, Hash] message
|
|
9
9
|
# The message to format
|
|
@@ -12,25 +12,25 @@ module LLM::Anthropic::Format
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
##
|
|
15
|
-
#
|
|
15
|
+
# Adapts the message for the Anthropic chat completions API
|
|
16
16
|
# @return [Hash]
|
|
17
|
-
def
|
|
17
|
+
def adapt
|
|
18
18
|
catch(:abort) do
|
|
19
19
|
if Hash === message
|
|
20
|
-
{role: message[:role], content:
|
|
20
|
+
{role: message[:role], content: adapt_content(message[:content])}
|
|
21
21
|
else
|
|
22
|
-
|
|
22
|
+
adapt_message
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def adapt_message
|
|
30
30
|
if message.tool_call?
|
|
31
31
|
{role: message.role, content: message.extra[:original_tool_calls]}
|
|
32
32
|
else
|
|
33
|
-
{role: message.role, content:
|
|
33
|
+
{role: message.role, content: adapt_content(content)}
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -39,41 +39,41 @@ module LLM::Anthropic::Format
|
|
|
39
39
|
# The content to format
|
|
40
40
|
# @return [String, Hash]
|
|
41
41
|
# The formatted content
|
|
42
|
-
def
|
|
42
|
+
def adapt_content(content)
|
|
43
43
|
case content
|
|
44
44
|
when Hash
|
|
45
45
|
content.empty? ? throw(:abort, nil) : [content]
|
|
46
46
|
when Array
|
|
47
|
-
content.empty? ? throw(:abort, nil) : content.flat_map {
|
|
47
|
+
content.empty? ? throw(:abort, nil) : content.flat_map { adapt_content(_1) }
|
|
48
48
|
when LLM::Object
|
|
49
|
-
|
|
49
|
+
adapt_object(content)
|
|
50
50
|
when String
|
|
51
51
|
[{type: :text, text: content}]
|
|
52
52
|
when LLM::Response
|
|
53
|
-
|
|
53
|
+
adapt_remote_file(content)
|
|
54
54
|
when LLM::Message
|
|
55
|
-
|
|
55
|
+
adapt_content(content.content)
|
|
56
56
|
when LLM::Function::Return
|
|
57
|
-
[{type: "tool_result", tool_use_id: content.id, content: [{type: :text, text:
|
|
57
|
+
[{type: "tool_result", tool_use_id: content.id, content: [{type: :text, text: LLM.json.dump(content.value)}]}]
|
|
58
58
|
else
|
|
59
59
|
prompt_error!(content)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def adapt_object(object)
|
|
64
64
|
case object.kind
|
|
65
65
|
when :image_url
|
|
66
66
|
[{type: :image, source: {type: "url", url: object.value.to_s}}]
|
|
67
67
|
when :local_file
|
|
68
|
-
|
|
68
|
+
adapt_local_file(object.value)
|
|
69
69
|
when :remote_file
|
|
70
|
-
|
|
70
|
+
adapt_remote_file(object.value)
|
|
71
71
|
else
|
|
72
72
|
prompt_error!(content)
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
def
|
|
76
|
+
def adapt_local_file(file)
|
|
77
77
|
if file.image?
|
|
78
78
|
[{type: :image, source: {type: "base64", media_type: file.mime_type, data: file.to_b64}}]
|
|
79
79
|
elsif file.pdf?
|
|
@@ -85,7 +85,7 @@ module LLM::Anthropic::Format
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def
|
|
88
|
+
def adapt_remote_file(file)
|
|
89
89
|
prompt_error!(file) unless file.file?
|
|
90
90
|
[{type: file.file_type, source: {type: :file, file_id: file.id}}]
|
|
91
91
|
end
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
class LLM::Anthropic
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
module
|
|
7
|
-
require_relative "
|
|
6
|
+
module RequestAdapter
|
|
7
|
+
require_relative "request_adapter/completion"
|
|
8
8
|
|
|
9
9
|
##
|
|
10
10
|
# @param [Array<LLM::Message>] messages
|
|
11
|
-
# The messages to
|
|
11
|
+
# The messages to adapt
|
|
12
12
|
# @return [Array<Hash>]
|
|
13
|
-
def
|
|
13
|
+
def adapt(messages, mode: nil)
|
|
14
14
|
messages.filter_map do
|
|
15
|
-
|
|
15
|
+
Completion.new(_1).adapt
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -21,9 +21,9 @@ class LLM::Anthropic
|
|
|
21
21
|
##
|
|
22
22
|
# @param [Hash] params
|
|
23
23
|
# @return [Hash]
|
|
24
|
-
def
|
|
24
|
+
def adapt_tools(tools)
|
|
25
25
|
return {} unless tools&.any?
|
|
26
|
-
{tools: tools.map { _1.respond_to?(:
|
|
26
|
+
{tools: tools.map { _1.respond_to?(:adapt) ? _1.adapt(self) : _1 }}
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Anthropic::ResponseAdapter
|
|
4
|
+
module Completion
|
|
5
|
+
##
|
|
6
|
+
# (see LLM::Contract::Completion#messages)
|
|
7
|
+
def messages
|
|
8
|
+
adapt_choices
|
|
9
|
+
end
|
|
10
|
+
alias_method :choices, :messages
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# (see LLM::Contract::Completion#input_tokens)
|
|
14
|
+
def input_tokens
|
|
15
|
+
body.usage["input_tokens"] || 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# (see LLM::Contract::Completion#output_tokens)
|
|
20
|
+
def output_tokens
|
|
21
|
+
body.usage["output_tokens"] || 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# (see LLM::Contract::Completion#total_tokens)
|
|
26
|
+
def total_tokens
|
|
27
|
+
input_tokens + output_tokens
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# (see LLM::Contract::Completion#usage)
|
|
32
|
+
def usage
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# (see LLM::Contract::Completion#model)
|
|
38
|
+
def model
|
|
39
|
+
body.model
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def adapt_choices
|
|
45
|
+
texts.map.with_index do |choice, index|
|
|
46
|
+
extra = {
|
|
47
|
+
index:, response: self,
|
|
48
|
+
tool_calls: adapt_tool_calls(tools), original_tool_calls: tools
|
|
49
|
+
}
|
|
50
|
+
LLM::Message.new(role, choice["text"], extra)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def adapt_tool_calls(tools)
|
|
55
|
+
(tools || []).filter_map do |tool|
|
|
56
|
+
{id: tool.id, name: tool.name, arguments: tool.input}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parts = body.content
|
|
61
|
+
def texts = @texts ||= parts.select { _1["type"] == "text" }
|
|
62
|
+
def tools = @tools ||= parts.select { _1["type"] == "tool_use" }
|
|
63
|
+
|
|
64
|
+
include LLM::Contract::Completion
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::Anthropic::
|
|
3
|
+
module LLM::Anthropic::ResponseAdapter
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::Anthropic::
|
|
5
|
+
# The {LLM::Anthropic::ResponseAdapter::WebSearch LLM::Anthropic::ResponseAdapter::WebSearch}
|
|
6
6
|
# module provides methods for accessing web search results from a web search
|
|
7
7
|
# tool call made via the {LLM::Provider#web_search LLM::Provider#web_search}
|
|
8
8
|
# method.
|
|
@@ -11,7 +11,7 @@ module LLM::Anthropic::Response
|
|
|
11
11
|
# Returns one or more search results
|
|
12
12
|
# @return [Array<LLM::Object>]
|
|
13
13
|
def search_results
|
|
14
|
-
LLM::Object.
|
|
14
|
+
LLM::Object.from(
|
|
15
15
|
content
|
|
16
16
|
.select { _1["type"] == "web_search_tool_result" }
|
|
17
17
|
.flat_map { |n| n.content.map { _1.slice(:title, :url) } }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Anthropic
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
require_relative "response_adapter/completion"
|
|
8
|
+
require_relative "response_adapter/enumerable"
|
|
9
|
+
require_relative "response_adapter/file"
|
|
10
|
+
require_relative "response_adapter/web_search"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
16
|
+
# @param [Symbol] type
|
|
17
|
+
# @return [LLM::Response]
|
|
18
|
+
def adapt(res, type:)
|
|
19
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
20
|
+
response.extend(select(type))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# @api private
|
|
25
|
+
def select(type)
|
|
26
|
+
case type
|
|
27
|
+
when :completion then LLM::Anthropic::ResponseAdapter::Completion
|
|
28
|
+
when :enumerable then LLM::Anthropic::ResponseAdapter::Enumerable
|
|
29
|
+
when :file then LLM::Anthropic::ResponseAdapter::File
|
|
30
|
+
when :web_search then LLM::Anthropic::ResponseAdapter::WebSearch
|
|
31
|
+
else
|
|
32
|
+
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -6,14 +6,14 @@ class LLM::Anthropic
|
|
|
6
6
|
class StreamParser
|
|
7
7
|
##
|
|
8
8
|
# Returns the fully constructed response body
|
|
9
|
-
# @return [
|
|
9
|
+
# @return [Hash]
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
# @param [#<<] io An IO-like object
|
|
14
14
|
# @return [LLM::Anthropic::StreamParser]
|
|
15
15
|
def initialize(io)
|
|
16
|
-
@body =
|
|
16
|
+
@body = {"role" => "assistant", "content" => []}
|
|
17
17
|
@io = io
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -33,10 +33,10 @@ class LLM::Anthropic
|
|
|
33
33
|
@body["content"][chunk["index"]] = chunk["content_block"]
|
|
34
34
|
elsif chunk["type"] == "content_block_delta"
|
|
35
35
|
if chunk["delta"]["type"] == "text_delta"
|
|
36
|
-
@body
|
|
36
|
+
@body["content"][chunk["index"]]["text"] << chunk["delta"]["text"]
|
|
37
37
|
@io << chunk["delta"]["text"] if @io.respond_to?(:<<)
|
|
38
38
|
elsif chunk["delta"]["type"] == "input_json_delta"
|
|
39
|
-
content = @body
|
|
39
|
+
content = @body["content"][chunk["index"]]
|
|
40
40
|
if Hash === content["input"]
|
|
41
41
|
content["input"] = chunk["delta"]["partial_json"]
|
|
42
42
|
else
|
|
@@ -46,9 +46,9 @@ class LLM::Anthropic
|
|
|
46
46
|
elsif chunk["type"] == "message_delta"
|
|
47
47
|
merge_message!(chunk["delta"])
|
|
48
48
|
elsif chunk["type"] == "content_block_stop"
|
|
49
|
-
content = @body
|
|
49
|
+
content = @body["content"][chunk["index"]]
|
|
50
50
|
if content["input"]
|
|
51
|
-
content["input"] =
|
|
51
|
+
content["input"] = LLM.json.load(content["input"])
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|