llm.rb 0.2.1 → 0.3.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 +264 -110
- data/lib/llm/buffer.rb +83 -0
- data/lib/llm/chat.rb +131 -0
- data/lib/llm/file.rb +26 -40
- data/lib/llm/http_client.rb +10 -5
- data/lib/llm/message.rb +14 -8
- data/lib/llm/mime.rb +54 -0
- data/lib/llm/multipart.rb +98 -0
- data/lib/llm/provider.rb +96 -19
- data/lib/llm/providers/anthropic/error_handler.rb +2 -0
- data/lib/llm/providers/anthropic/format.rb +2 -0
- data/lib/llm/providers/anthropic/response_parser.rb +3 -1
- data/lib/llm/providers/anthropic.rb +14 -5
- data/lib/llm/providers/gemini/audio.rb +77 -0
- data/lib/llm/providers/gemini/error_handler.rb +2 -0
- data/lib/llm/providers/gemini/files.rb +160 -0
- data/lib/llm/providers/gemini/format.rb +12 -6
- data/lib/llm/providers/gemini/images.rb +99 -0
- data/lib/llm/providers/gemini/response_parser.rb +27 -1
- data/lib/llm/providers/gemini.rb +62 -6
- data/lib/llm/providers/ollama/error_handler.rb +2 -0
- data/lib/llm/providers/ollama/format.rb +13 -5
- data/lib/llm/providers/ollama/response_parser.rb +3 -1
- data/lib/llm/providers/ollama.rb +30 -7
- data/lib/llm/providers/openai/audio.rb +97 -0
- data/lib/llm/providers/openai/error_handler.rb +2 -0
- data/lib/llm/providers/openai/files.rb +148 -0
- data/lib/llm/providers/openai/format.rb +21 -8
- data/lib/llm/providers/openai/images.rb +109 -0
- data/lib/llm/providers/openai/response_parser.rb +58 -5
- data/lib/llm/providers/openai/responses.rb +78 -0
- data/lib/llm/providers/openai.rb +52 -6
- data/lib/llm/providers/voyageai.rb +2 -2
- data/lib/llm/response/audio.rb +13 -0
- data/lib/llm/response/audio_transcription.rb +14 -0
- data/lib/llm/response/audio_translation.rb +14 -0
- data/lib/llm/response/download_file.rb +15 -0
- data/lib/llm/response/file.rb +42 -0
- data/lib/llm/response/filelist.rb +18 -0
- data/lib/llm/response/image.rb +29 -0
- data/lib/llm/response/output.rb +56 -0
- data/lib/llm/response.rb +18 -6
- data/lib/llm/utils.rb +19 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +5 -2
- data/llm.gemspec +1 -6
- data/spec/anthropic/completion_spec.rb +1 -1
- data/spec/gemini/completion_spec.rb +1 -1
- data/spec/gemini/conversation_spec.rb +31 -0
- data/spec/gemini/files_spec.rb +124 -0
- data/spec/gemini/images_spec.rb +47 -0
- data/spec/llm/conversation_spec.rb +101 -61
- data/spec/ollama/completion_spec.rb +1 -1
- data/spec/ollama/conversation_spec.rb +31 -0
- data/spec/openai/audio_spec.rb +55 -0
- data/spec/openai/completion_spec.rb +1 -1
- data/spec/openai/files_spec.rb +150 -0
- data/spec/openai/images_spec.rb +95 -0
- data/spec/openai/responses_spec.rb +51 -0
- data/spec/setup.rb +8 -0
- metadata +31 -49
- data/LICENSE.txt +0 -21
- data/lib/llm/conversation.rb +0 -90
- data/lib/llm/message_queue.rb +0 -54
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# The {LLM::OpenAI::Files LLM::OpenAI::Files} class provides a files
|
6
|
+
# object for interacting with [OpenAI's Files API](https://platform.openai.com/docs/api-reference/files/create).
|
7
|
+
# The files API allows a client to upload files for use with OpenAI's models
|
8
|
+
# and API endpoints. OpenAI supports multiple file formats, including text
|
9
|
+
# files, CSV files, JSON files, and more.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# #!/usr/bin/env ruby
|
13
|
+
# require "llm"
|
14
|
+
#
|
15
|
+
# llm = LLM.openai(ENV["KEY"])
|
16
|
+
# bot = LLM::Chat.new(llm).lazy
|
17
|
+
# file = llm.files.create file: LLM::File("/documents/freebsd.pdf")
|
18
|
+
# bot.chat(file)
|
19
|
+
# bot.chat("Describe the document")
|
20
|
+
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
21
|
+
# @example
|
22
|
+
# #!/usr/bin/env ruby
|
23
|
+
# require "llm"
|
24
|
+
#
|
25
|
+
# llm = LLM.openai(ENV["KEY"])
|
26
|
+
# bot = LLM::Chat.new(llm).lazy
|
27
|
+
# file = llm.files.create file: LLM::File("/documents/openbsd.pdf")
|
28
|
+
# bot.chat(["Describe the document I sent to you", file])
|
29
|
+
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
30
|
+
class Files
|
31
|
+
##
|
32
|
+
# Returns a new Files object
|
33
|
+
# @param provider [LLM::Provider]
|
34
|
+
# @return [LLM::OpenAI::Files]
|
35
|
+
def initialize(provider)
|
36
|
+
@provider = provider
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# List all files
|
41
|
+
# @example
|
42
|
+
# llm = LLM.openai(ENV["KEY"])
|
43
|
+
# res = llm.files.all
|
44
|
+
# res.each do |file|
|
45
|
+
# print "id: ", file.id, "\n"
|
46
|
+
# end
|
47
|
+
# @see https://platform.openai.com/docs/api-reference/files/list OpenAI docs
|
48
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
49
|
+
# @raise (see LLM::HTTPClient#request)
|
50
|
+
# @return [LLM::Response::FileList]
|
51
|
+
def all(**params)
|
52
|
+
query = URI.encode_www_form(params)
|
53
|
+
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
54
|
+
res = request(http, req)
|
55
|
+
LLM::Response::FileList.new(res).tap { |filelist|
|
56
|
+
files = filelist.body["data"].map { OpenStruct.from_hash(_1) }
|
57
|
+
filelist.files = files
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Create a file
|
63
|
+
# @example
|
64
|
+
# llm = LLM.openai(ENV["KEY"])
|
65
|
+
# res = llm.files.create file: LLM::File("/documents/haiku.txt"),
|
66
|
+
# @see https://platform.openai.com/docs/api-reference/files/create OpenAI docs
|
67
|
+
# @param [File] file The file
|
68
|
+
# @param [String] purpose The purpose of the file (see OpenAI docs)
|
69
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
70
|
+
# @raise (see LLM::HTTPClient#request)
|
71
|
+
# @return [LLM::Response::File]
|
72
|
+
def create(file:, purpose: "assistants", **params)
|
73
|
+
multi = LLM::Multipart.new(params.merge!(file:, purpose:))
|
74
|
+
req = Net::HTTP::Post.new("/v1/files", headers)
|
75
|
+
req["content-type"] = multi.content_type
|
76
|
+
req.body = multi.body
|
77
|
+
res = request(http, req)
|
78
|
+
LLM::Response::File.new(res)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Get a file
|
83
|
+
# @example
|
84
|
+
# llm = LLM.openai(ENV["KEY"])
|
85
|
+
# res = llm.files.get(file: "file-1234567890")
|
86
|
+
# print "id: ", res.id, "\n"
|
87
|
+
# @see https://platform.openai.com/docs/api-reference/files/get OpenAI docs
|
88
|
+
# @param [#id, #to_s] file The file ID
|
89
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
90
|
+
# @raise (see LLM::HTTPClient#request)
|
91
|
+
# @return [LLM::Response::File]
|
92
|
+
def get(file:, **params)
|
93
|
+
file_id = file.respond_to?(:id) ? file.id : file
|
94
|
+
query = URI.encode_www_form(params)
|
95
|
+
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
96
|
+
res = request(http, req)
|
97
|
+
LLM::Response::File.new(res)
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Download the content of a file
|
102
|
+
# @example
|
103
|
+
# llm = LLM.openai(ENV["KEY"])
|
104
|
+
# res = llm.files.download(file: "file-1234567890")
|
105
|
+
# File.binwrite "haiku1.txt", res.file.read
|
106
|
+
# print res.file.read, "\n"
|
107
|
+
# @see https://platform.openai.com/docs/api-reference/files/content OpenAI docs
|
108
|
+
# @param [#id, #to_s] file The file ID
|
109
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
110
|
+
# @raise (see LLM::HTTPClient#request)
|
111
|
+
# @return [LLM::Response::DownloadFile]
|
112
|
+
def download(file:, **params)
|
113
|
+
query = URI.encode_www_form(params)
|
114
|
+
file_id = file.respond_to?(:id) ? file.id : file
|
115
|
+
req = Net::HTTP::Get.new("/v1/files/#{file_id}/content?#{query}", headers)
|
116
|
+
io = StringIO.new("".b)
|
117
|
+
res = request(http, req) { |res| res.read_body { |chunk| io << chunk } }
|
118
|
+
LLM::Response::DownloadFile.new(res).tap { _1.file = io }
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Delete a file
|
123
|
+
# @example
|
124
|
+
# llm = LLM.openai(ENV["KEY"])
|
125
|
+
# res = llm.files.delete(file: "file-1234567890")
|
126
|
+
# print res.deleted, "\n"
|
127
|
+
# @see https://platform.openai.com/docs/api-reference/files/delete OpenAI docs
|
128
|
+
# @param [#id, #to_s] file The file ID
|
129
|
+
# @raise (see LLM::HTTPClient#request)
|
130
|
+
# @return [OpenStruct] Response body
|
131
|
+
def delete(file:)
|
132
|
+
file_id = file.respond_to?(:id) ? file.id : file
|
133
|
+
req = Net::HTTP::Delete.new("/v1/files/#{file_id}", headers)
|
134
|
+
res = request(http, req)
|
135
|
+
OpenStruct.from_hash JSON.parse(res.body)
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def http
|
141
|
+
@provider.instance_variable_get(:@http)
|
142
|
+
end
|
143
|
+
|
144
|
+
[:headers, :request].each do |m|
|
145
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -1,17 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# @private
|
4
6
|
module Format
|
5
7
|
##
|
6
8
|
# @param [Array<LLM::Message>] messages
|
7
9
|
# The messages to format
|
10
|
+
# @param [Symbol] mode
|
11
|
+
# The mode to format the messages for
|
8
12
|
# @return [Array<Hash>]
|
9
|
-
def format(messages)
|
13
|
+
def format(messages, mode)
|
10
14
|
messages.map do
|
11
15
|
if Hash === _1
|
12
|
-
{role: _1[:role], content: format_content(_1[:content])}
|
16
|
+
{role: _1[:role], content: format_content(_1[:content], mode)}
|
13
17
|
else
|
14
|
-
{role: _1.role, content: format_content(_1.content)}
|
18
|
+
{role: _1.role, content: format_content(_1.content, mode)}
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
@@ -23,11 +27,20 @@ class LLM::OpenAI
|
|
23
27
|
# The content to format
|
24
28
|
# @return [String, Hash]
|
25
29
|
# The formatted content
|
26
|
-
def format_content(content)
|
27
|
-
if
|
28
|
-
|
29
|
-
|
30
|
-
content
|
30
|
+
def format_content(content, mode)
|
31
|
+
if mode == :complete
|
32
|
+
case content
|
33
|
+
when Array then content.flat_map { format_content(_1, mode) }
|
34
|
+
when URI then [{type: :image_url, image_url: {url: content.to_s}}]
|
35
|
+
else [{type: :text, text: content.to_s}]
|
36
|
+
end
|
37
|
+
elsif mode == :response
|
38
|
+
case content
|
39
|
+
when Array then content.flat_map { format_content(_1, mode) }
|
40
|
+
when URI then [{type: :image_url, image_url: {url: content.to_s}}]
|
41
|
+
when LLM::Response::File then [{type: :input_file, file_id: content.id}]
|
42
|
+
else [{type: :input_text, text: content.to_s}]
|
43
|
+
end
|
31
44
|
end
|
32
45
|
end
|
33
46
|
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# The {LLM::OpenAI::Images LLM::OpenAI::Images} class provides an images
|
6
|
+
# object for interacting with [OpenAI's images API](https://platform.openai.com/docs/api-reference/images).
|
7
|
+
# OpenAI supports multiple response formats: temporary URLs, or binary strings
|
8
|
+
# encoded in base64. The default is to return temporary URLs.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# #!/usr/bin/env ruby
|
12
|
+
# require "llm"
|
13
|
+
# require "open-uri"
|
14
|
+
# require "fileutils"
|
15
|
+
#
|
16
|
+
# llm = LLM.openai(ENV["KEY"])
|
17
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
18
|
+
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
19
|
+
# "rocket.png"
|
20
|
+
# @example
|
21
|
+
# #!/usr/bin/env ruby
|
22
|
+
# require "llm"
|
23
|
+
#
|
24
|
+
# llm = LLM.openai(ENV["KEY"])
|
25
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
26
|
+
# response_format: "b64_json"
|
27
|
+
# File.binwrite("rocket.png", res.images[0].binary)
|
28
|
+
class Images
|
29
|
+
##
|
30
|
+
# Returns a new Images object
|
31
|
+
# @param provider [LLM::Provider]
|
32
|
+
# @return [LLM::OpenAI::Responses]
|
33
|
+
def initialize(provider)
|
34
|
+
@provider = provider
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Create an image
|
39
|
+
# @example
|
40
|
+
# llm = LLM.openai(ENV["KEY"])
|
41
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
42
|
+
# p res.urls
|
43
|
+
# @see https://platform.openai.com/docs/api-reference/images/create OpenAI docs
|
44
|
+
# @param [String] prompt The prompt
|
45
|
+
# @param [String] model The model to use
|
46
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
47
|
+
# @raise (see LLM::HTTPClient#request)
|
48
|
+
# @return [LLM::Response::Image]
|
49
|
+
def create(prompt:, model: "dall-e-3", **params)
|
50
|
+
req = Net::HTTP::Post.new("/v1/images/generations", headers)
|
51
|
+
req.body = JSON.dump({prompt:, n: 1, model:}.merge!(params))
|
52
|
+
res = request(http, req)
|
53
|
+
LLM::Response::Image.new(res).extend(response_parser)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Create image variations
|
58
|
+
# @example
|
59
|
+
# llm = LLM.openai(ENV["KEY"])
|
60
|
+
# res = llm.images.create_variation(image: LLM::File("/images/hat.png"), n: 5)
|
61
|
+
# p res.urls
|
62
|
+
# @see https://platform.openai.com/docs/api-reference/images/createVariation OpenAI docs
|
63
|
+
# @param [File] image The image to create variations from
|
64
|
+
# @param [String] model The model to use
|
65
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
66
|
+
# @raise (see LLM::HTTPClient#request)
|
67
|
+
# @return [LLM::Response::Image]
|
68
|
+
def create_variation(image:, model: "dall-e-2", **params)
|
69
|
+
multi = LLM::Multipart.new(params.merge!(image:, model:))
|
70
|
+
req = Net::HTTP::Post.new("/v1/images/variations", headers)
|
71
|
+
req["content-type"] = multi.content_type
|
72
|
+
req.body = multi.body
|
73
|
+
res = request(http, req)
|
74
|
+
LLM::Response::Image.new(res).extend(response_parser)
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Edit an image
|
79
|
+
# @example
|
80
|
+
# llm = LLM.openai(ENV["KEY"])
|
81
|
+
# res = llm.images.edit(image: LLM::File("/images/hat.png"), prompt: "A cat wearing this hat")
|
82
|
+
# p res.urls
|
83
|
+
# @see https://platform.openai.com/docs/api-reference/images/createEdit OpenAI docs
|
84
|
+
# @param [File] image The image to edit
|
85
|
+
# @param [String] prompt The prompt
|
86
|
+
# @param [String] model The model to use
|
87
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
88
|
+
# @raise (see LLM::HTTPClient#request)
|
89
|
+
# @return [LLM::Response::Image]
|
90
|
+
def edit(image:, prompt:, model: "dall-e-2", **params)
|
91
|
+
multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:))
|
92
|
+
req = Net::HTTP::Post.new("/v1/images/edits", headers)
|
93
|
+
req["content-type"] = multi.content_type
|
94
|
+
req.body = multi.body
|
95
|
+
res = request(http, req)
|
96
|
+
LLM::Response::Image.new(res).extend(response_parser)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def http
|
102
|
+
@provider.instance_variable_get(:@http)
|
103
|
+
end
|
104
|
+
|
105
|
+
[:response_parser, :headers, :request].each do |m|
|
106
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# @private
|
4
6
|
module ResponseParser
|
5
7
|
##
|
6
8
|
# @param [Hash] body
|
@@ -22,16 +24,67 @@ class LLM::OpenAI
|
|
22
24
|
def parse_completion(body)
|
23
25
|
{
|
24
26
|
model: body["model"],
|
25
|
-
choices: body["choices"].map do
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
choices: body["choices"].map.with_index do
|
28
|
+
extra = {
|
29
|
+
index: _2, response: self,
|
30
|
+
logprobs: _1["logprobs"]
|
31
|
+
}
|
32
|
+
LLM::Message.new(*_1["message"].values_at("role", "content"), extra)
|
30
33
|
end,
|
31
34
|
prompt_tokens: body.dig("usage", "prompt_tokens"),
|
32
35
|
completion_tokens: body.dig("usage", "completion_tokens"),
|
33
36
|
total_tokens: body.dig("usage", "total_tokens")
|
34
37
|
}
|
35
38
|
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# @param [Hash] body
|
42
|
+
# The response body from the LLM provider
|
43
|
+
# @return [Hash]
|
44
|
+
def parse_output_response(body)
|
45
|
+
{
|
46
|
+
id: body["id"],
|
47
|
+
model: body["model"],
|
48
|
+
input_tokens: body.dig("usage", "input_tokens"),
|
49
|
+
output_tokens: body.dig("usage", "output_tokens"),
|
50
|
+
total_tokens: body.dig("usage", "total_tokens"),
|
51
|
+
outputs: body["output"].filter_map.with_index do |output, index|
|
52
|
+
next unless output["content"]
|
53
|
+
extra = {
|
54
|
+
index:, response: self,
|
55
|
+
contents: output["content"],
|
56
|
+
annotations: output["annotations"]
|
57
|
+
}
|
58
|
+
LLM::Message.new(output["role"], text(output), extra)
|
59
|
+
end
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# @param [Hash] body
|
65
|
+
# The response body from the LLM provider
|
66
|
+
# @return [Hash]
|
67
|
+
def parse_image(body)
|
68
|
+
{
|
69
|
+
urls: body["data"].filter_map { _1["url"] },
|
70
|
+
images: body["data"].filter_map do
|
71
|
+
next unless _1["b64_json"]
|
72
|
+
OpenStruct.from_hash(
|
73
|
+
mime_type: nil,
|
74
|
+
encoded: _1["b64_json"],
|
75
|
+
binary: _1["b64_json"].unpack1("m0")
|
76
|
+
)
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def text(output)
|
84
|
+
output["content"]
|
85
|
+
.select { _1["type"] == "output_text" }
|
86
|
+
.map { _1["text"] }
|
87
|
+
.join("\n")
|
88
|
+
end
|
36
89
|
end
|
37
90
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# The {LLM::OpenAI::Responses LLM::OpenAI::Responses} class provides a responses
|
6
|
+
# object for interacting with [OpenAI's response API](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses).
|
7
|
+
# @example
|
8
|
+
# llm = LLM.openai(ENV["KEY"])
|
9
|
+
# res1 = llm.responses.create "Your task is to help me with math", :developer
|
10
|
+
# res2 = llm.responses.create "5 + 5 = ?", :user, previous_response_id: res1.id
|
11
|
+
# [res1,res2].each { llm.responses.delete(_1) }
|
12
|
+
class Responses
|
13
|
+
include Format
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns a new Responses object
|
17
|
+
# @param provider [LLM::Provider]
|
18
|
+
# @return [LLM::OpenAI::Responses]
|
19
|
+
def initialize(provider)
|
20
|
+
@provider = provider
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Create a response
|
25
|
+
# @see https://platform.openai.com/docs/api-reference/responses/create OpenAI docs
|
26
|
+
# @param prompt (see LLM::Provider#complete)
|
27
|
+
# @param role (see LLM::Provider#complete)
|
28
|
+
# @param model (see LLM::Provider#complete)
|
29
|
+
# @param [Hash] params Response params
|
30
|
+
# @raise (see LLM::HTTPClient#request)
|
31
|
+
# @return [LLM::Response::Output]
|
32
|
+
def create(prompt, role = :user, model: "gpt-4o-mini", **params)
|
33
|
+
params = {model:}.merge!(params)
|
34
|
+
req = Net::HTTP::Post.new("/v1/responses", headers)
|
35
|
+
messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
36
|
+
req.body = JSON.dump({input: format(messages, :response)}.merge!(params))
|
37
|
+
res = request(http, req)
|
38
|
+
LLM::Response::Output.new(res).extend(response_parser)
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Get a response
|
43
|
+
# @see https://platform.openai.com/docs/api-reference/responses/get OpenAI docs
|
44
|
+
# @param [#id, #to_s] response Response ID
|
45
|
+
# @raise (see LLM::HTTPClient#request)
|
46
|
+
# @return [LLM::Response::Output]
|
47
|
+
def get(response, **params)
|
48
|
+
response_id = response.respond_to?(:id) ? response.id : response
|
49
|
+
query = URI.encode_www_form(params)
|
50
|
+
req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
|
51
|
+
res = request(http, req)
|
52
|
+
LLM::Response::Output.new(res).extend(response_parser)
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Deletes a response
|
57
|
+
# @see https://platform.openai.com/docs/api-reference/responses/delete OpenAI docs
|
58
|
+
# @param [#id, #to_s] response Response ID
|
59
|
+
# @raise (see LLM::HTTPClient#request)
|
60
|
+
# @return [OpenStruct] Response body
|
61
|
+
def delete(response)
|
62
|
+
response_id = response.respond_to?(:id) ? response.id : response
|
63
|
+
req = Net::HTTP::Delete.new("/v1/responses/#{response_id}", headers)
|
64
|
+
res = request(http, req)
|
65
|
+
OpenStruct.from_hash JSON.parse(res.body)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def http
|
71
|
+
@provider.instance_variable_get(:@http)
|
72
|
+
end
|
73
|
+
|
74
|
+
[:response_parser, :headers, :request].each do |m|
|
75
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/llm/providers/openai.rb
CHANGED
@@ -5,9 +5,13 @@ module LLM
|
|
5
5
|
# The OpenAI class implements a provider for
|
6
6
|
# [OpenAI](https://platform.openai.com/)
|
7
7
|
class OpenAI < Provider
|
8
|
+
require_relative "openai/format"
|
8
9
|
require_relative "openai/error_handler"
|
9
10
|
require_relative "openai/response_parser"
|
10
|
-
require_relative "openai/
|
11
|
+
require_relative "openai/responses"
|
12
|
+
require_relative "openai/images"
|
13
|
+
require_relative "openai/audio"
|
14
|
+
require_relative "openai/files"
|
11
15
|
include Format
|
12
16
|
|
13
17
|
HOST = "api.openai.com"
|
@@ -19,29 +23,71 @@ module LLM
|
|
19
23
|
end
|
20
24
|
|
21
25
|
##
|
26
|
+
# Provides an embedding
|
27
|
+
# @see https://platform.openai.com/docs/api-reference/embeddings/create OpenAI docs
|
22
28
|
# @param input (see LLM::Provider#embed)
|
29
|
+
# @param model (see LLM::Provider#embed)
|
30
|
+
# @param params (see LLM::Provider#embed)
|
31
|
+
# @raise (see LLM::HTTPClient#request)
|
23
32
|
# @return (see LLM::Provider#embed)
|
24
|
-
def embed(input, **params)
|
33
|
+
def embed(input, model: "text-embedding-3-small", **params)
|
25
34
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
26
|
-
req.body = JSON.dump({input:, model:
|
35
|
+
req.body = JSON.dump({input:, model:}.merge!(params))
|
27
36
|
res = request(@http, req)
|
28
37
|
Response::Embedding.new(res).extend(response_parser)
|
29
38
|
end
|
30
39
|
|
31
40
|
##
|
41
|
+
# Provides an interface to the chat completions API
|
32
42
|
# @see https://platform.openai.com/docs/api-reference/chat/create OpenAI docs
|
33
43
|
# @param prompt (see LLM::Provider#complete)
|
34
44
|
# @param role (see LLM::Provider#complete)
|
45
|
+
# @param model (see LLM::Provider#complete)
|
46
|
+
# @param params (see LLM::Provider#complete)
|
47
|
+
# @example (see LLM::Provider#complete)
|
48
|
+
# @raise (see LLM::HTTPClient#request)
|
35
49
|
# @return (see LLM::Provider#complete)
|
36
|
-
def complete(prompt, role = :user, **params)
|
37
|
-
params = {model:
|
50
|
+
def complete(prompt, role = :user, model: "gpt-4o-mini", **params)
|
51
|
+
params = {model:}.merge!(params)
|
38
52
|
req = Net::HTTP::Post.new("/v1/chat/completions", headers)
|
39
53
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
40
|
-
req.body = JSON.dump({messages: format(messages)}.merge!(params))
|
54
|
+
req.body = JSON.dump({messages: format(messages, :complete)}.merge!(params))
|
41
55
|
res = request(@http, req)
|
42
56
|
Response::Completion.new(res).extend(response_parser)
|
43
57
|
end
|
44
58
|
|
59
|
+
##
|
60
|
+
# Provides an interface to OpenAI's response API
|
61
|
+
# @see https://platform.openai.com/docs/api-reference/responses/create OpenAI docs
|
62
|
+
# @return [LLM::OpenAI::Responses]
|
63
|
+
def responses
|
64
|
+
LLM::OpenAI::Responses.new(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Provides an interface to OpenAI's image generation API
|
69
|
+
# @see https://platform.openai.com/docs/api-reference/images/create OpenAI docs
|
70
|
+
# @return [LLM::OpenAI::Images]
|
71
|
+
def images
|
72
|
+
LLM::OpenAI::Images.new(self)
|
73
|
+
end
|
74
|
+
|
75
|
+
##
|
76
|
+
# Provides an interface to OpenAI's audio generation API
|
77
|
+
# @see https://platform.openai.com/docs/api-reference/audio/createSpeech OpenAI docs
|
78
|
+
# @return [LLM::OpenAI::Audio]
|
79
|
+
def audio
|
80
|
+
LLM::OpenAI::Audio.new(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Provides an interface to OpenAI's files API
|
85
|
+
# @see https://platform.openai.com/docs/api-reference/files/create OpenAI docs
|
86
|
+
# @return [LLM::OpenAI::Files]
|
87
|
+
def files
|
88
|
+
LLM::OpenAI::Files.new(self)
|
89
|
+
end
|
90
|
+
|
45
91
|
##
|
46
92
|
# @return (see LLM::Provider#assistant_role)
|
47
93
|
def assistant_role
|
@@ -17,9 +17,9 @@ module LLM
|
|
17
17
|
# [Anthropic's recommendation](https://docs.anthropic.com/en/docs/build-with-claude/embeddings)
|
18
18
|
# @param input (see LLM::Provider#embed)
|
19
19
|
# @return (see LLM::Provider#embed)
|
20
|
-
def embed(input, **params)
|
20
|
+
def embed(input, model: "voyage-2", **params)
|
21
21
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
22
|
-
req.body = JSON.dump({input:, model:
|
22
|
+
req.body = JSON.dump({input:, model:}.merge!(params))
|
23
23
|
res = request(@http, req)
|
24
24
|
Response::Embedding.new(res).extend(response_parser)
|
25
25
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::Audio LLM::Response::Audio} class represents an
|
6
|
+
# audio file that has been returned by a provider. It wraps an IO object
|
7
|
+
# that can be used to read the contents of an audio stream (as binary data).
|
8
|
+
class Response::Audio < Response
|
9
|
+
##
|
10
|
+
# @return [StringIO]
|
11
|
+
attr_accessor :audio
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::AudioTranscription LLM::Response::AudioTranscription}
|
6
|
+
# class represents an audio transcription that has been returned by
|
7
|
+
# a provider (eg OpenAI, Gemini, etc)
|
8
|
+
class Response::AudioTranscription < Response
|
9
|
+
##
|
10
|
+
# Returns the text of the transcription
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :text
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::AudioTranslation LLM::Response::AudioTranslation}
|
6
|
+
# class represents an audio translation that has been returned by
|
7
|
+
# a provider (eg OpenAI, Gemini, etc)
|
8
|
+
class Response::AudioTranslation < Response
|
9
|
+
##
|
10
|
+
# Returns the text of the translation
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :text
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::DownloadFile LLM::Response::DownloadFile} class
|
6
|
+
# represents the contents of a file that has been returned by a
|
7
|
+
# provider. It wraps an IO object that can be used to read the file
|
8
|
+
# contents.
|
9
|
+
class Response::DownloadFile < Response
|
10
|
+
##
|
11
|
+
# Returns a StringIO object
|
12
|
+
# @return [StringIO]
|
13
|
+
attr_accessor :file
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LLM
|
4
|
+
##
|
5
|
+
# The {LLM::Response::File LLM::Response::File} class represents a file
|
6
|
+
# that has been uploaded to a provider. Its properties are delegated
|
7
|
+
# to the underlying response body, and vary by provider.
|
8
|
+
class Response::File < Response
|
9
|
+
##
|
10
|
+
# Returns a normalized response body
|
11
|
+
# @return [Hash]
|
12
|
+
def body
|
13
|
+
@_body ||= if super["file"]
|
14
|
+
super["file"].transform_keys { snakecase(_1) }
|
15
|
+
else
|
16
|
+
super.transform_keys { snakecase(_1) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
# @return [String]
|
22
|
+
def inspect
|
23
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} body=#{body}>"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
include LLM::Utils
|
29
|
+
|
30
|
+
def respond_to_missing?(m, _)
|
31
|
+
body.key?(m.to_s) || super
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(m, *args, &block)
|
35
|
+
if body.key?(m.to_s)
|
36
|
+
body[m.to_s]
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|