llm.rb 0.8.0 → 0.9.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 +62 -48
- data/lib/llm/{chat → bot}/builder.rb +1 -1
- data/lib/llm/bot/conversable.rb +31 -0
- data/lib/llm/{chat → bot}/prompt/completion.rb +14 -4
- data/lib/llm/{chat → bot}/prompt/respond.rb +16 -5
- data/lib/llm/{chat.rb → bot.rb} +48 -66
- data/lib/llm/error.rb +22 -22
- data/lib/llm/event_handler.rb +44 -0
- data/lib/llm/eventstream/event.rb +69 -0
- data/lib/llm/eventstream/parser.rb +88 -0
- data/lib/llm/eventstream.rb +8 -0
- data/lib/llm/function.rb +9 -12
- data/lib/llm/object/builder.rb +8 -9
- data/lib/llm/object/kernel.rb +1 -1
- data/lib/llm/object.rb +7 -1
- data/lib/llm/provider.rb +61 -26
- data/lib/llm/providers/anthropic/error_handler.rb +3 -3
- data/lib/llm/providers/anthropic/models.rb +3 -7
- data/lib/llm/providers/anthropic/response_parser/completion_parser.rb +3 -3
- data/lib/llm/providers/anthropic/response_parser.rb +1 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +66 -0
- data/lib/llm/providers/anthropic.rb +9 -4
- data/lib/llm/providers/gemini/error_handler.rb +4 -4
- data/lib/llm/providers/gemini/files.rb +12 -15
- data/lib/llm/providers/gemini/images.rb +4 -8
- data/lib/llm/providers/gemini/models.rb +3 -7
- data/lib/llm/providers/gemini/stream_parser.rb +69 -0
- data/lib/llm/providers/gemini.rb +19 -11
- data/lib/llm/providers/ollama/error_handler.rb +3 -3
- data/lib/llm/providers/ollama/format/completion_format.rb +1 -1
- data/lib/llm/providers/ollama/models.rb +3 -7
- data/lib/llm/providers/ollama/stream_parser.rb +44 -0
- data/lib/llm/providers/ollama.rb +13 -6
- data/lib/llm/providers/openai/audio.rb +5 -9
- data/lib/llm/providers/openai/error_handler.rb +3 -3
- data/lib/llm/providers/openai/files.rb +12 -15
- data/lib/llm/providers/openai/images.rb +8 -11
- data/lib/llm/providers/openai/models.rb +3 -7
- data/lib/llm/providers/openai/moderations.rb +3 -7
- data/lib/llm/providers/openai/response_parser/completion_parser.rb +3 -3
- data/lib/llm/providers/openai/response_parser.rb +3 -0
- data/lib/llm/providers/openai/responses.rb +10 -12
- data/lib/llm/providers/openai/stream_parser.rb +77 -0
- data/lib/llm/providers/openai.rb +11 -7
- data/lib/llm/providers/voyageai/error_handler.rb +3 -3
- data/lib/llm/providers/voyageai.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +4 -2
- data/llm.gemspec +1 -1
- metadata +30 -25
- data/lib/llm/chat/conversable.rb +0 -53
- /data/lib/{json → llm/json}/schema/array.rb +0 -0
- /data/lib/{json → llm/json}/schema/boolean.rb +0 -0
- /data/lib/{json → llm/json}/schema/integer.rb +0 -0
- /data/lib/{json → llm/json}/schema/leaf.rb +0 -0
- /data/lib/{json → llm/json}/schema/null.rb +0 -0
- /data/lib/{json → llm/json}/schema/number.rb +0 -0
- /data/lib/{json → llm/json}/schema/object.rb +0 -0
- /data/lib/{json → llm/json}/schema/string.rb +0 -0
- /data/lib/{json → llm/json}/schema/version.rb +0 -0
- /data/lib/{json → llm/json}/schema.rb +0 -0
@@ -13,23 +13,24 @@ class LLM::Gemini
|
|
13
13
|
# in the prompt over and over again (which could be the case in a
|
14
14
|
# multi-turn conversation).
|
15
15
|
#
|
16
|
-
# @example
|
16
|
+
# @example example #1
|
17
17
|
# #!/usr/bin/env ruby
|
18
18
|
# require "llm"
|
19
19
|
#
|
20
20
|
# llm = LLM.gemini(ENV["KEY"])
|
21
|
-
# bot = LLM::
|
21
|
+
# bot = LLM::Bot.new(llm)
|
22
22
|
# file = llm.files.create file: "/audio/haiku.mp3"
|
23
23
|
# bot.chat(file)
|
24
24
|
# bot.chat("Describe the audio file I sent to you")
|
25
25
|
# bot.chat("The audio file is the first message I sent to you.")
|
26
26
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
27
|
-
#
|
27
|
+
#
|
28
|
+
# @example example #2
|
28
29
|
# #!/usr/bin/env ruby
|
29
30
|
# require "llm"
|
30
31
|
#
|
31
32
|
# llm = LLM.gemini(ENV["KEY"])
|
32
|
-
# bot = LLM::
|
33
|
+
# bot = LLM::Bot.new(llm)
|
33
34
|
# file = llm.files.create file: "/audio/haiku.mp3"
|
34
35
|
# bot.chat(["Describe the audio file I sent to you", file])
|
35
36
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
@@ -57,7 +58,7 @@ class LLM::Gemini
|
|
57
58
|
def all(**params)
|
58
59
|
query = URI.encode_www_form(params.merge!(key: key))
|
59
60
|
req = Net::HTTP::Get.new("/v1beta/files?#{query}", headers)
|
60
|
-
res = request
|
61
|
+
res = execute(request: req)
|
61
62
|
LLM::Response::FileList.new(res).tap { |filelist|
|
62
63
|
files = filelist.body["files"]&.map do |file|
|
63
64
|
file = file.transform_keys { snakecase(_1) }
|
@@ -85,7 +86,7 @@ class LLM::Gemini
|
|
85
86
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
86
87
|
file.with_io do |io|
|
87
88
|
set_body_stream(req, io)
|
88
|
-
res = request
|
89
|
+
res = execute(request: req)
|
89
90
|
LLM::Response::File.new(res)
|
90
91
|
end
|
91
92
|
end
|
@@ -105,7 +106,7 @@ class LLM::Gemini
|
|
105
106
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
106
107
|
query = URI.encode_www_form(params.merge!(key: key))
|
107
108
|
req = Net::HTTP::Get.new("/v1beta/#{file_id}?#{query}", headers)
|
108
|
-
res = request
|
109
|
+
res = execute(request: req)
|
109
110
|
LLM::Response::File.new(res)
|
110
111
|
end
|
111
112
|
|
@@ -123,7 +124,7 @@ class LLM::Gemini
|
|
123
124
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
124
125
|
query = URI.encode_www_form(params.merge!(key: key))
|
125
126
|
req = Net::HTTP::Delete.new("/v1beta/#{file_id}?#{query}", headers)
|
126
|
-
request
|
127
|
+
execute(request: req)
|
127
128
|
end
|
128
129
|
|
129
130
|
##
|
@@ -144,20 +145,16 @@ class LLM::Gemini
|
|
144
145
|
req["X-Goog-Upload-Header-Content-Length"] = file.bytesize
|
145
146
|
req["X-Goog-Upload-Header-Content-Type"] = file.mime_type
|
146
147
|
req.body = JSON.dump(file: {display_name: File.basename(file.path)})
|
147
|
-
res = request
|
148
|
+
res = execute(request: req)
|
148
149
|
res["x-goog-upload-url"]
|
149
150
|
end
|
150
151
|
|
151
|
-
def http
|
152
|
-
@provider.instance_variable_get(:@http)
|
153
|
-
end
|
154
|
-
|
155
152
|
def key
|
156
153
|
@provider.instance_variable_get(:@key)
|
157
154
|
end
|
158
155
|
|
159
|
-
[:headers, :
|
160
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
156
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
157
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
161
158
|
end
|
162
159
|
end
|
163
160
|
end
|
@@ -47,7 +47,7 @@ class LLM::Gemini
|
|
47
47
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
48
48
|
}.merge!(params))
|
49
49
|
req.body = body
|
50
|
-
res = request
|
50
|
+
res = execute(request: req)
|
51
51
|
LLM::Response::Image.new(res).extend(response_parser)
|
52
52
|
end
|
53
53
|
|
@@ -72,7 +72,7 @@ class LLM::Gemini
|
|
72
72
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
73
73
|
}.merge!(params)).b
|
74
74
|
set_body_stream(req, StringIO.new(body))
|
75
|
-
res = request
|
75
|
+
res = execute(request: req)
|
76
76
|
LLM::Response::Image.new(res).extend(response_parser)
|
77
77
|
end
|
78
78
|
|
@@ -93,12 +93,8 @@ class LLM::Gemini
|
|
93
93
|
@provider.instance_variable_get(:@key)
|
94
94
|
end
|
95
95
|
|
96
|
-
|
97
|
-
@provider.
|
98
|
-
end
|
99
|
-
|
100
|
-
[:response_parser, :headers, :request, :set_body_stream].each do |m|
|
101
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
96
|
+
[:response_parser, :headers, :execute, :set_body_stream].each do |m|
|
97
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
102
98
|
end
|
103
99
|
end
|
104
100
|
end
|
@@ -42,7 +42,7 @@ 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 = request
|
45
|
+
res = execute(request: req)
|
46
46
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
47
47
|
models = modellist.body["models"].map do |model|
|
48
48
|
model = model.transform_keys { snakecase(_1) }
|
@@ -54,16 +54,12 @@ class LLM::Gemini
|
|
54
54
|
|
55
55
|
private
|
56
56
|
|
57
|
-
def http
|
58
|
-
@provider.instance_variable_get(:@http)
|
59
|
-
end
|
60
|
-
|
61
57
|
def key
|
62
58
|
@provider.instance_variable_get(:@key)
|
63
59
|
end
|
64
60
|
|
65
|
-
[:headers, :
|
66
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
61
|
+
[:headers, :execute].each do |m|
|
62
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
67
63
|
end
|
68
64
|
end
|
69
65
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Gemini
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class StreamParser
|
7
|
+
##
|
8
|
+
# Returns the fully constructed response body
|
9
|
+
# @return [LLM::Object]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
##
|
13
|
+
# @param [#<<] io An IO-like object
|
14
|
+
# @return [LLM::Gemini::StreamParser]
|
15
|
+
def initialize(io)
|
16
|
+
@body = LLM::Object.new
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [Hash] chunk
|
22
|
+
# @return [LLM::Gemini::StreamParser]
|
23
|
+
def parse!(chunk)
|
24
|
+
tap { merge!(chunk) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def merge!(chunk)
|
30
|
+
chunk.each do |key, value|
|
31
|
+
if key == "candidates"
|
32
|
+
@body.candidates ||= []
|
33
|
+
merge_candidates!(value)
|
34
|
+
else
|
35
|
+
@body[key] = value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def merge_candidates!(candidates)
|
41
|
+
candidates.each.with_index do |candidate, i|
|
42
|
+
if @body.candidates[i].nil?
|
43
|
+
merge_one(@body.candidates, candidate, i)
|
44
|
+
else
|
45
|
+
merge_two(@body.candidates, candidate, i)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge_one(candidates, candidate, i)
|
51
|
+
candidate
|
52
|
+
.dig("content", "parts")
|
53
|
+
&.filter_map { _1["text"] }
|
54
|
+
&.each { @io << _1 if @io.respond_to?(:<<) }
|
55
|
+
candidates[i] = candidate
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge_two(candidates, candidate, i)
|
59
|
+
parts = candidates[i].dig("content", "parts")
|
60
|
+
parts&.each&.with_index do |part, j|
|
61
|
+
if part["text"]
|
62
|
+
target = candidate["content"]["parts"][j]
|
63
|
+
part["text"] << target["text"]
|
64
|
+
@io << target["text"] if @io.respond_to?(:<<)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/llm/providers/gemini.rb
CHANGED
@@ -10,31 +10,34 @@ module LLM
|
|
10
10
|
# prompt for files under 20MB or via the Gemini Files API for
|
11
11
|
# files that are over 20MB
|
12
12
|
#
|
13
|
-
# @example
|
13
|
+
# @example example #1
|
14
14
|
# #!/usr/bin/env ruby
|
15
15
|
# require "llm"
|
16
16
|
#
|
17
17
|
# llm = LLM.gemini(ENV["KEY"])
|
18
|
-
# bot = LLM::
|
19
|
-
# bot.chat LLM
|
18
|
+
# bot = LLM::Bot.new(llm)
|
19
|
+
# bot.chat LLM.File("/images/capybara.png")
|
20
20
|
# bot.chat "Describe the image"
|
21
21
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
22
|
-
#
|
22
|
+
#
|
23
|
+
# @example example #2
|
23
24
|
# #!/usr/bin/env ruby
|
24
25
|
# require "llm"
|
25
26
|
#
|
26
27
|
# llm = LLM.gemini(ENV["KEY"])
|
27
|
-
# bot = LLM::
|
28
|
+
# bot = LLM::Bot.new(llm)
|
28
29
|
# bot.chat ["Describe the image", LLM::File("/images/capybara.png")]
|
29
30
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
30
31
|
class Gemini < Provider
|
31
32
|
require_relative "gemini/error_handler"
|
32
|
-
require_relative "gemini/response_parser"
|
33
33
|
require_relative "gemini/format"
|
34
|
+
require_relative "gemini/stream_parser"
|
35
|
+
require_relative "gemini/response_parser"
|
36
|
+
require_relative "gemini/models"
|
34
37
|
require_relative "gemini/images"
|
35
38
|
require_relative "gemini/files"
|
36
39
|
require_relative "gemini/audio"
|
37
|
-
|
40
|
+
|
38
41
|
include Format
|
39
42
|
|
40
43
|
HOST = "generativelanguage.googleapis.com"
|
@@ -57,7 +60,7 @@ module LLM
|
|
57
60
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
|
58
61
|
req = Net::HTTP::Post.new(path, headers)
|
59
62
|
req.body = JSON.dump({content: {parts: [{text: input}]}})
|
60
|
-
res = request
|
63
|
+
res = execute(request: req)
|
61
64
|
Response::Embedding.new(res).extend(response_parser)
|
62
65
|
end
|
63
66
|
|
@@ -74,14 +77,15 @@ module LLM
|
|
74
77
|
def complete(prompt, params = {})
|
75
78
|
params = {role: :user, model: default_model}.merge!(params)
|
76
79
|
params = [params, format_schema(params), format_tools(params)].inject({}, &:merge!).compact
|
77
|
-
role, model = [:role, :model].map { params.delete(_1) }
|
80
|
+
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
81
|
+
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
78
82
|
model.respond_to?(:id) ? model.id : model
|
79
|
-
path = ["/v1beta/models/#{model}",
|
83
|
+
path = ["/v1beta/models/#{model}", action].join(":")
|
80
84
|
req = Net::HTTP::Post.new(path, headers)
|
81
85
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
82
86
|
body = JSON.dump({contents: format(messages)}.merge!(params))
|
83
87
|
set_body_stream(req, StringIO.new(body))
|
84
|
-
res = request
|
88
|
+
res = execute(request: req, stream:)
|
85
89
|
Response::Completion.new(res).extend(response_parser)
|
86
90
|
end
|
87
91
|
|
@@ -140,6 +144,10 @@ module LLM
|
|
140
144
|
LLM::Gemini::ResponseParser
|
141
145
|
end
|
142
146
|
|
147
|
+
def stream_parser
|
148
|
+
LLM::Gemini::StreamParser
|
149
|
+
end
|
150
|
+
|
143
151
|
def error_handler
|
144
152
|
LLM::Gemini::ErrorHandler
|
145
153
|
end
|
@@ -23,11 +23,11 @@ class LLM::Ollama
|
|
23
23
|
def raise_error!
|
24
24
|
case res
|
25
25
|
when Net::HTTPUnauthorized
|
26
|
-
raise LLM::
|
26
|
+
raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
|
27
27
|
when Net::HTTPTooManyRequests
|
28
|
-
raise LLM::
|
28
|
+
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
29
29
|
else
|
30
|
-
raise LLM::
|
30
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -63,7 +63,7 @@ module LLM::Ollama::Format
|
|
63
63
|
elsif returns.any?
|
64
64
|
returns.map { {role: "tool", tool_call_id: _1.id, content: JSON.dump(_1.value)} }
|
65
65
|
else
|
66
|
-
content.flat_map { {role: message.role
|
66
|
+
content.flat_map { {role: message.role}.merge(format_content(_1)) }
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
@@ -43,7 +43,7 @@ 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 = request
|
46
|
+
res = execute(request: req)
|
47
47
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
48
48
|
models = modellist.body["models"].map do |model|
|
49
49
|
model = model.transform_keys { snakecase(_1) }
|
@@ -55,12 +55,8 @@ class LLM::Ollama
|
|
55
55
|
|
56
56
|
private
|
57
57
|
|
58
|
-
|
59
|
-
@provider.
|
60
|
-
end
|
61
|
-
|
62
|
-
[:headers, :request].each do |m|
|
63
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
58
|
+
[:headers, :execute].each do |m|
|
59
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
64
60
|
end
|
65
61
|
end
|
66
62
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Ollama
|
4
|
+
##
|
5
|
+
# @private
|
6
|
+
class StreamParser
|
7
|
+
##
|
8
|
+
# Returns the fully constructed response body
|
9
|
+
# @return [LLM::Object]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
##
|
13
|
+
# @return [LLM::OpenAI::Chunk]
|
14
|
+
def initialize(io)
|
15
|
+
@body = LLM::Object.new
|
16
|
+
@io = io
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param [Hash] chunk
|
21
|
+
# @return [LLM::OpenAI::Chunk]
|
22
|
+
def parse!(chunk)
|
23
|
+
tap { merge!(chunk) }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def merge!(chunk)
|
29
|
+
chunk.each do |key, value|
|
30
|
+
if key == "message"
|
31
|
+
if @body[key]
|
32
|
+
@body[key]["content"] << value["content"]
|
33
|
+
@io << value["content"] if @io.respond_to?(:<<)
|
34
|
+
else
|
35
|
+
@body[key] = value
|
36
|
+
@io << value["content"] if @io.respond_to?(:<<)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@body[key] = value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/llm/providers/ollama.rb
CHANGED
@@ -14,15 +14,17 @@ module LLM
|
|
14
14
|
# require "llm"
|
15
15
|
#
|
16
16
|
# llm = LLM.ollama(nil)
|
17
|
-
# bot = LLM::
|
17
|
+
# bot = LLM::Bot.new(llm, model: "llava")
|
18
18
|
# bot.chat LLM::File("/images/capybara.png")
|
19
19
|
# bot.chat "Describe the image"
|
20
20
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
21
21
|
class Ollama < Provider
|
22
22
|
require_relative "ollama/error_handler"
|
23
|
-
require_relative "ollama/response_parser"
|
24
23
|
require_relative "ollama/format"
|
24
|
+
require_relative "ollama/stream_parser"
|
25
|
+
require_relative "ollama/response_parser"
|
25
26
|
require_relative "ollama/models"
|
27
|
+
|
26
28
|
include Format
|
27
29
|
|
28
30
|
HOST = "localhost"
|
@@ -44,7 +46,7 @@ module LLM
|
|
44
46
|
params = {model:}.merge!(params)
|
45
47
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
46
48
|
req.body = JSON.dump({input:}.merge!(params))
|
47
|
-
res = request
|
49
|
+
res = execute(request: req)
|
48
50
|
Response::Embedding.new(res).extend(response_parser)
|
49
51
|
end
|
50
52
|
|
@@ -59,14 +61,15 @@ module LLM
|
|
59
61
|
# When given an object a provider does not understand
|
60
62
|
# @return (see LLM::Provider#complete)
|
61
63
|
def complete(prompt, params = {})
|
62
|
-
params = {role: :user, model: default_model, stream:
|
64
|
+
params = {role: :user, model: default_model, stream: true}.merge!(params)
|
63
65
|
params = [params, {format: params[:schema]}, format_tools(params)].inject({}, &:merge!).compact
|
64
|
-
role = params.delete(:role)
|
66
|
+
role, stream = params.delete(:role), params.delete(:stream)
|
67
|
+
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
65
68
|
req = Net::HTTP::Post.new("/api/chat", headers)
|
66
69
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
67
70
|
body = JSON.dump({messages: [format(messages)].flatten}.merge!(params))
|
68
71
|
set_body_stream(req, StringIO.new(body))
|
69
|
-
res = request
|
72
|
+
res = execute(request: req, stream:)
|
70
73
|
Response::Completion.new(res).extend(response_parser)
|
71
74
|
end
|
72
75
|
|
@@ -105,6 +108,10 @@ module LLM
|
|
105
108
|
LLM::Ollama::ResponseParser
|
106
109
|
end
|
107
110
|
|
111
|
+
def stream_parser
|
112
|
+
LLM::Ollama::StreamParser
|
113
|
+
end
|
114
|
+
|
108
115
|
def error_handler
|
109
116
|
LLM::Ollama::ErrorHandler
|
110
117
|
end
|
@@ -35,7 +35,7 @@ class LLM::OpenAI
|
|
35
35
|
req = Net::HTTP::Post.new("/v1/audio/speech", headers)
|
36
36
|
req.body = JSON.dump({input:, voice:, model:, response_format:}.merge!(params))
|
37
37
|
io = StringIO.new("".b)
|
38
|
-
res = request
|
38
|
+
res = execute(request: req) { _1.read_body { |chunk| io << chunk } }
|
39
39
|
LLM::Response::Audio.new(res).tap { _1.audio = io }
|
40
40
|
end
|
41
41
|
|
@@ -56,7 +56,7 @@ class LLM::OpenAI
|
|
56
56
|
req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
|
57
57
|
req["content-type"] = multi.content_type
|
58
58
|
set_body_stream(req, multi.body)
|
59
|
-
res = request
|
59
|
+
res = execute(request: req)
|
60
60
|
LLM::Response::AudioTranscription.new(res).tap { _1.text = _1.body["text"] }
|
61
61
|
end
|
62
62
|
|
@@ -78,18 +78,14 @@ class LLM::OpenAI
|
|
78
78
|
req = Net::HTTP::Post.new("/v1/audio/translations", headers)
|
79
79
|
req["content-type"] = multi.content_type
|
80
80
|
set_body_stream(req, multi.body)
|
81
|
-
res = request
|
81
|
+
res = execute(request: req)
|
82
82
|
LLM::Response::AudioTranslation.new(res).tap { _1.text = _1.body["text"] }
|
83
83
|
end
|
84
84
|
|
85
85
|
private
|
86
86
|
|
87
|
-
|
88
|
-
@provider.
|
89
|
-
end
|
90
|
-
|
91
|
-
[:headers, :request, :set_body_stream].each do |m|
|
92
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
87
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
88
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
93
89
|
end
|
94
90
|
end
|
95
91
|
end
|
@@ -23,11 +23,11 @@ class LLM::OpenAI
|
|
23
23
|
def raise_error!
|
24
24
|
case res
|
25
25
|
when Net::HTTPUnauthorized
|
26
|
-
raise LLM::
|
26
|
+
raise LLM::UnauthorizedError.new { _1.response = res }, "Authentication error"
|
27
27
|
when Net::HTTPTooManyRequests
|
28
|
-
raise LLM::
|
28
|
+
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
29
29
|
else
|
30
|
-
raise LLM::
|
30
|
+
raise LLM::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -8,22 +8,23 @@ class LLM::OpenAI
|
|
8
8
|
# and API endpoints. OpenAI supports multiple file formats, including text
|
9
9
|
# files, CSV files, JSON files, and more.
|
10
10
|
#
|
11
|
-
# @example
|
11
|
+
# @example example #1
|
12
12
|
# #!/usr/bin/env ruby
|
13
13
|
# require "llm"
|
14
14
|
#
|
15
15
|
# llm = LLM.openai(ENV["KEY"])
|
16
|
-
# bot = LLM::
|
16
|
+
# bot = LLM::Bot.new(llm)
|
17
17
|
# file = llm.files.create file: "/documents/freebsd.pdf"
|
18
18
|
# bot.chat(file)
|
19
19
|
# bot.chat("Describe the document")
|
20
20
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
21
|
-
#
|
21
|
+
#
|
22
|
+
# @example example #2
|
22
23
|
# #!/usr/bin/env ruby
|
23
24
|
# require "llm"
|
24
25
|
#
|
25
26
|
# llm = LLM.openai(ENV["KEY"])
|
26
|
-
# bot = LLM::
|
27
|
+
# bot = LLM::Bot.new(llm)
|
27
28
|
# file = llm.files.create file: "/documents/openbsd.pdf"
|
28
29
|
# bot.chat(["Describe the document I sent to you", file])
|
29
30
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
@@ -51,7 +52,7 @@ class LLM::OpenAI
|
|
51
52
|
def all(**params)
|
52
53
|
query = URI.encode_www_form(params)
|
53
54
|
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
54
|
-
res = request
|
55
|
+
res = execute(request: req)
|
55
56
|
LLM::Response::FileList.new(res).tap { |filelist|
|
56
57
|
files = filelist.body["data"].map { LLM::Object.from_hash(_1) }
|
57
58
|
filelist.files = files
|
@@ -74,7 +75,7 @@ class LLM::OpenAI
|
|
74
75
|
req = Net::HTTP::Post.new("/v1/files", headers)
|
75
76
|
req["content-type"] = multi.content_type
|
76
77
|
set_body_stream(req, multi.body)
|
77
|
-
res = request
|
78
|
+
res = execute(request: req)
|
78
79
|
LLM::Response::File.new(res)
|
79
80
|
end
|
80
81
|
|
@@ -93,7 +94,7 @@ class LLM::OpenAI
|
|
93
94
|
file_id = file.respond_to?(:id) ? file.id : file
|
94
95
|
query = URI.encode_www_form(params)
|
95
96
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
96
|
-
res = request
|
97
|
+
res = execute(request: req)
|
97
98
|
LLM::Response::File.new(res)
|
98
99
|
end
|
99
100
|
|
@@ -114,7 +115,7 @@ class LLM::OpenAI
|
|
114
115
|
file_id = file.respond_to?(:id) ? file.id : file
|
115
116
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
|
116
117
|
io = StringIO.new("".b)
|
117
|
-
res = request
|
118
|
+
res = execute(request: req) { |res| res.read_body { |chunk| io << chunk } }
|
118
119
|
LLM::Response::DownloadFile.new(res).tap { _1.file = io }
|
119
120
|
end
|
120
121
|
|
@@ -131,18 +132,14 @@ class LLM::OpenAI
|
|
131
132
|
def delete(file:)
|
132
133
|
file_id = file.respond_to?(:id) ? file.id : file
|
133
134
|
req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
|
134
|
-
res = request
|
135
|
+
res = execute(request: req)
|
135
136
|
LLM::Object.from_hash JSON.parse(res.body)
|
136
137
|
end
|
137
138
|
|
138
139
|
private
|
139
140
|
|
140
|
-
|
141
|
-
@provider.
|
142
|
-
end
|
143
|
-
|
144
|
-
[:headers, :request, :set_body_stream].each do |m|
|
145
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
141
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
142
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
146
143
|
end
|
147
144
|
end
|
148
145
|
end
|
@@ -7,7 +7,7 @@ class LLM::OpenAI
|
|
7
7
|
# OpenAI supports multiple response formats: temporary URLs, or binary strings
|
8
8
|
# encoded in base64. The default is to return temporary URLs.
|
9
9
|
#
|
10
|
-
# @example
|
10
|
+
# @example example #1
|
11
11
|
# #!/usr/bin/env ruby
|
12
12
|
# require "llm"
|
13
13
|
# require "open-uri"
|
@@ -17,7 +17,8 @@ class LLM::OpenAI
|
|
17
17
|
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
18
18
|
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
19
19
|
# "rocket.png"
|
20
|
-
#
|
20
|
+
#
|
21
|
+
# @example example #2
|
21
22
|
# #!/usr/bin/env ruby
|
22
23
|
# require "llm"
|
23
24
|
#
|
@@ -49,7 +50,7 @@ class LLM::OpenAI
|
|
49
50
|
def create(prompt:, model: "dall-e-3", **params)
|
50
51
|
req = Net::HTTP::Post.new("/v1/images/generations", headers)
|
51
52
|
req.body = JSON.dump({prompt:, n: 1, model:}.merge!(params))
|
52
|
-
res = request
|
53
|
+
res = execute(request: req)
|
53
54
|
LLM::Response::Image.new(res).extend(response_parser)
|
54
55
|
end
|
55
56
|
|
@@ -71,7 +72,7 @@ class LLM::OpenAI
|
|
71
72
|
req = Net::HTTP::Post.new("/v1/images/variations", headers)
|
72
73
|
req["content-type"] = multi.content_type
|
73
74
|
set_body_stream(req, multi.body)
|
74
|
-
res = request
|
75
|
+
res = execute(request: req)
|
75
76
|
LLM::Response::Image.new(res).extend(response_parser)
|
76
77
|
end
|
77
78
|
|
@@ -94,18 +95,14 @@ class LLM::OpenAI
|
|
94
95
|
req = Net::HTTP::Post.new("/v1/images/edits", headers)
|
95
96
|
req["content-type"] = multi.content_type
|
96
97
|
set_body_stream(req, multi.body)
|
97
|
-
res = request
|
98
|
+
res = execute(request: req)
|
98
99
|
LLM::Response::Image.new(res).extend(response_parser)
|
99
100
|
end
|
100
101
|
|
101
102
|
private
|
102
103
|
|
103
|
-
|
104
|
-
@provider.
|
105
|
-
end
|
106
|
-
|
107
|
-
[:response_parser, :headers, :request, :set_body_stream].each do |m|
|
108
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
104
|
+
[:response_parser, :headers, :execute, :set_body_stream].each do |m|
|
105
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
109
106
|
end
|
110
107
|
end
|
111
108
|
end
|
@@ -40,7 +40,7 @@ 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 = request
|
43
|
+
res = execute(request: req)
|
44
44
|
LLM::Response::ModelList.new(res).tap { |modellist|
|
45
45
|
models = modellist.body["data"].map do |model|
|
46
46
|
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
@@ -51,12 +51,8 @@ class LLM::OpenAI
|
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
|
55
|
-
@provider.
|
56
|
-
end
|
57
|
-
|
58
|
-
[:headers, :request, :set_body_stream].each do |m|
|
59
|
-
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
54
|
+
[:headers, :execute, :set_body_stream].each do |m|
|
55
|
+
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
60
56
|
end
|
61
57
|
end
|
62
58
|
end
|