llm.rb 0.2.1 → 0.3.1
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 +318 -110
- data/lib/llm/buffer.rb +83 -0
- data/lib/llm/chat.rb +131 -0
- data/lib/llm/error.rb +3 -3
- data/lib/llm/file.rb +36 -40
- data/lib/llm/message.rb +21 -8
- data/lib/llm/mime.rb +54 -0
- data/lib/llm/multipart.rb +100 -0
- data/lib/llm/provider.rb +123 -21
- data/lib/llm/providers/anthropic/error_handler.rb +3 -1
- 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 +4 -2
- data/lib/llm/providers/gemini/files.rb +162 -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 +3 -1
- 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 +3 -1
- data/lib/llm/providers/openai/files.rb +148 -0
- data/lib/llm/providers/openai/format.rb +22 -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 +85 -0
- data/lib/llm/providers/openai.rb +52 -6
- data/lib/llm/providers/voyageai/error_handler.rb +1 -1
- 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 +107 -62
- 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 +5 -4
- data/spec/openai/files_spec.rb +204 -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 -50
- data/LICENSE.txt +0 -21
- data/lib/llm/conversation.rb +0 -90
- data/lib/llm/http_client.rb +0 -29
- data/lib/llm/message_queue.rb +0 -54
data/lib/llm/providers/ollama.rb
CHANGED
@@ -2,8 +2,22 @@
|
|
2
2
|
|
3
3
|
module LLM
|
4
4
|
##
|
5
|
-
# The Ollama class implements a provider for
|
6
|
-
#
|
5
|
+
# The Ollama class implements a provider for [Ollama](https://ollama.ai/).
|
6
|
+
#
|
7
|
+
# This provider supports a wide range of models, it is relatively
|
8
|
+
# straight forward to run on your own hardware, and includes multi-modal
|
9
|
+
# models that can process images and text. See the example for a demonstration
|
10
|
+
# of a multi-modal model by the name `llava`
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# #!/usr/bin/env ruby
|
14
|
+
# require "llm"
|
15
|
+
#
|
16
|
+
# llm = LLM.ollama(nil)
|
17
|
+
# bot = LLM::Chat.new(llm, model: "llava").lazy
|
18
|
+
# bot.chat LLM::File("/images/capybara.png")
|
19
|
+
# bot.chat "Describe the image"
|
20
|
+
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
7
21
|
class Ollama < Provider
|
8
22
|
require_relative "ollama/error_handler"
|
9
23
|
require_relative "ollama/response_parser"
|
@@ -19,10 +33,14 @@ module LLM
|
|
19
33
|
end
|
20
34
|
|
21
35
|
##
|
36
|
+
# Provides an embedding
|
22
37
|
# @param input (see LLM::Provider#embed)
|
38
|
+
# @param model (see LLM::Provider#embed)
|
39
|
+
# @param params (see LLM::Provider#embed)
|
40
|
+
# @raise (see LLM::Provider#request)
|
23
41
|
# @return (see LLM::Provider#embed)
|
24
|
-
def embed(input, **params)
|
25
|
-
params = {model:
|
42
|
+
def embed(input, model: "llama3.2", **params)
|
43
|
+
params = {model:}.merge!(params)
|
26
44
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
27
45
|
req.body = JSON.dump({input:}.merge!(params))
|
28
46
|
res = request(@http, req)
|
@@ -30,15 +48,20 @@ module LLM
|
|
30
48
|
end
|
31
49
|
|
32
50
|
##
|
51
|
+
# Provides an interface to the chat completions API
|
33
52
|
# @see https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion Ollama docs
|
34
53
|
# @param prompt (see LLM::Provider#complete)
|
35
54
|
# @param role (see LLM::Provider#complete)
|
55
|
+
# @param model (see LLM::Provider#complete)
|
56
|
+
# @param params (see LLM::Provider#complete)
|
57
|
+
# @example (see LLM::Provider#complete)
|
58
|
+
# @raise (see LLM::Provider#request)
|
36
59
|
# @return (see LLM::Provider#complete)
|
37
|
-
def complete(prompt, role = :user, **params)
|
38
|
-
params = {model
|
60
|
+
def complete(prompt, role = :user, model: "llama3.2", **params)
|
61
|
+
params = {model:, stream: false}.merge!(params)
|
39
62
|
req = Net::HTTP::Post.new("/api/chat", headers)
|
40
63
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
41
|
-
req.body = JSON.dump({messages: messages
|
64
|
+
req.body = JSON.dump({messages: format(messages)}.merge!(params))
|
42
65
|
res = request(@http, req)
|
43
66
|
Response::Completion.new(res).extend(response_parser)
|
44
67
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# The {LLM::OpenAI::Audio LLM::OpenAI::Audio} class provides an audio
|
6
|
+
# object for interacting with [OpenAI's audio API](https://platform.openai.com/docs/api-reference/audio/createSpeech).
|
7
|
+
# @example
|
8
|
+
# llm = LLM.openai(ENV["KEY"])
|
9
|
+
# res = llm.audio.create_speech(input: "A dog on a rocket to the moon")
|
10
|
+
# File.binwrite("rocket.mp3", res.audio.string)
|
11
|
+
class Audio
|
12
|
+
require "stringio"
|
13
|
+
|
14
|
+
##
|
15
|
+
# Returns a new Audio object
|
16
|
+
# @param provider [LLM::Provider]
|
17
|
+
# @return [LLM::OpenAI::Responses]
|
18
|
+
def initialize(provider)
|
19
|
+
@provider = provider
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Create an audio track
|
24
|
+
# @example
|
25
|
+
# llm = LLM.openai(ENV["KEY"])
|
26
|
+
# res = llm.images.create_speech(input: "A dog on a rocket to the moon")
|
27
|
+
# File.binwrite("rocket.mp3", res.audio.string)
|
28
|
+
# @see https://platform.openai.com/docs/api-reference/audio/createSpeech OpenAI docs
|
29
|
+
# @param [String] input The text input
|
30
|
+
# @param [String] voice The voice to use
|
31
|
+
# @param [String] model The model to use
|
32
|
+
# @param [String] response_format The response format
|
33
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
34
|
+
# @raise (see LLM::Provider#request)
|
35
|
+
# @return [LLM::Response::Audio]
|
36
|
+
def create_speech(input:, voice: "alloy", model: "gpt-4o-mini-tts", response_format: "mp3", **params)
|
37
|
+
req = Net::HTTP::Post.new("/v1/audio/speech", headers)
|
38
|
+
req.body = JSON.dump({input:, voice:, model:, response_format:}.merge!(params))
|
39
|
+
io = StringIO.new("".b)
|
40
|
+
res = request(http, req) { _1.read_body { |chunk| io << chunk } }
|
41
|
+
LLM::Response::Audio.new(res).tap { _1.audio = io }
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Create an audio transcription
|
46
|
+
# @example
|
47
|
+
# llm = LLM.openai(ENV["KEY"])
|
48
|
+
# res = llm.audio.create_transcription(file: LLM::File("/rocket.mp3"))
|
49
|
+
# res.text # => "A dog on a rocket to the moon"
|
50
|
+
# @see https://platform.openai.com/docs/api-reference/audio/createTranscription OpenAI docs
|
51
|
+
# @param [LLM::File] file The input audio
|
52
|
+
# @param [String] model The model to use
|
53
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
54
|
+
# @raise (see LLM::Provider#request)
|
55
|
+
# @return [LLM::Response::AudioTranscription]
|
56
|
+
def create_transcription(file:, model: "whisper-1", **params)
|
57
|
+
multi = LLM::Multipart.new(params.merge!(file:, model:))
|
58
|
+
req = Net::HTTP::Post.new("/v1/audio/transcriptions", headers)
|
59
|
+
req["content-type"] = multi.content_type
|
60
|
+
req.body_stream = multi.body
|
61
|
+
res = request(http, req)
|
62
|
+
LLM::Response::AudioTranscription.new(res).tap { _1.text = _1.body["text"] }
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Create an audio translation (in English)
|
67
|
+
# @example
|
68
|
+
# # Arabic => English
|
69
|
+
# llm = LLM.openai(ENV["KEY"])
|
70
|
+
# res = llm.audio.create_translation(file: LLM::File("/bismillah.mp3"))
|
71
|
+
# res.text # => "In the name of Allah, the Beneficent, the Merciful."
|
72
|
+
# @see https://platform.openai.com/docs/api-reference/audio/createTranslation OpenAI docs
|
73
|
+
# @param [LLM::File] file The input audio
|
74
|
+
# @param [String] model The model to use
|
75
|
+
# @param [Hash] params Other parameters (see OpenAI docs)
|
76
|
+
# @raise (see LLM::Provider#request)
|
77
|
+
# @return [LLM::Response::AudioTranslation]
|
78
|
+
def create_translation(file:, model: "whisper-1", **params)
|
79
|
+
multi = LLM::Multipart.new(params.merge!(file:, model:))
|
80
|
+
req = Net::HTTP::Post.new("/v1/audio/translations", headers)
|
81
|
+
req["content-type"] = multi.content_type
|
82
|
+
req.body_stream = multi.body
|
83
|
+
res = request(http, req)
|
84
|
+
LLM::Response::AudioTranslation.new(res).tap { _1.text = _1.body["text"] }
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def http
|
90
|
+
@provider.instance_variable_get(:@http)
|
91
|
+
end
|
92
|
+
|
93
|
+
[:headers, :request].each do |m|
|
94
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class LLM::OpenAI
|
4
|
+
##
|
5
|
+
# @private
|
4
6
|
class ErrorHandler
|
5
7
|
##
|
6
8
|
# @return [Net::HTTPResponse]
|
@@ -25,7 +27,7 @@ class LLM::OpenAI
|
|
25
27
|
when Net::HTTPTooManyRequests
|
26
28
|
raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
|
27
29
|
else
|
28
|
-
raise LLM::Error::
|
30
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
29
31
|
end
|
30
32
|
end
|
31
33
|
end
|
@@ -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::Provider#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::Provider#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_stream = 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::Provider#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::Provider#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::Provider#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,21 @@ 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
|
+
when LLM::Response::File then [{type: :file, file: {file_id: content.id}}]
|
36
|
+
else [{type: :text, text: content.to_s}]
|
37
|
+
end
|
38
|
+
elsif mode == :response
|
39
|
+
case content
|
40
|
+
when Array then content.flat_map { format_content(_1, mode) }
|
41
|
+
when URI then [{type: :image_url, image_url: {url: content.to_s}}]
|
42
|
+
when LLM::Response::File then [{type: :input_file, file_id: content.id}]
|
43
|
+
else [{type: :input_text, text: content.to_s}]
|
44
|
+
end
|
31
45
|
end
|
32
46
|
end
|
33
47
|
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::Provider#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::Provider#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_stream = 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::Provider#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_stream = 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,85 @@
|
|
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
|
+
# The responses API is similar to the chat completions API but it can maintain
|
8
|
+
# conversation state across multiple requests. This is useful when you want to
|
9
|
+
# save bandwidth and/or not maintain the message thread by yourself.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# #!/usr/bin/env ruby
|
13
|
+
# require "llm"
|
14
|
+
#
|
15
|
+
# llm = LLM.openai(ENV["KEY"])
|
16
|
+
# res1 = llm.responses.create "Your task is to help me with math", :developer
|
17
|
+
# res2 = llm.responses.create "5 + 5 = ?", :user, previous_response_id: res1.id
|
18
|
+
# [res1,res2].each { llm.responses.delete(_1) }
|
19
|
+
class Responses
|
20
|
+
include Format
|
21
|
+
|
22
|
+
##
|
23
|
+
# Returns a new Responses object
|
24
|
+
# @param provider [LLM::Provider]
|
25
|
+
# @return [LLM::OpenAI::Responses]
|
26
|
+
def initialize(provider)
|
27
|
+
@provider = provider
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Create a response
|
32
|
+
# @see https://platform.openai.com/docs/api-reference/responses/create OpenAI docs
|
33
|
+
# @param prompt (see LLM::Provider#complete)
|
34
|
+
# @param role (see LLM::Provider#complete)
|
35
|
+
# @param model (see LLM::Provider#complete)
|
36
|
+
# @param [Hash] params Response params
|
37
|
+
# @raise (see LLM::Provider#request)
|
38
|
+
# @return [LLM::Response::Output]
|
39
|
+
def create(prompt, role = :user, model: "gpt-4o-mini", **params)
|
40
|
+
params = {model:}.merge!(params)
|
41
|
+
req = Net::HTTP::Post.new("/v1/responses", headers)
|
42
|
+
messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
43
|
+
req.body = JSON.dump({input: format(messages, :response)}.merge!(params))
|
44
|
+
res = request(http, req)
|
45
|
+
LLM::Response::Output.new(res).extend(response_parser)
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Get a response
|
50
|
+
# @see https://platform.openai.com/docs/api-reference/responses/get OpenAI docs
|
51
|
+
# @param [#id, #to_s] response Response ID
|
52
|
+
# @raise (see LLM::Provider#request)
|
53
|
+
# @return [LLM::Response::Output]
|
54
|
+
def get(response, **params)
|
55
|
+
response_id = response.respond_to?(:id) ? response.id : response
|
56
|
+
query = URI.encode_www_form(params)
|
57
|
+
req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
|
58
|
+
res = request(http, req)
|
59
|
+
LLM::Response::Output.new(res).extend(response_parser)
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Deletes a response
|
64
|
+
# @see https://platform.openai.com/docs/api-reference/responses/delete OpenAI docs
|
65
|
+
# @param [#id, #to_s] response Response ID
|
66
|
+
# @raise (see LLM::Provider#request)
|
67
|
+
# @return [OpenStruct] Response body
|
68
|
+
def delete(response)
|
69
|
+
response_id = response.respond_to?(:id) ? response.id : response
|
70
|
+
req = Net::HTTP::Delete.new("/v1/responses/#{response_id}", headers)
|
71
|
+
res = request(http, req)
|
72
|
+
OpenStruct.from_hash JSON.parse(res.body)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def http
|
78
|
+
@provider.instance_variable_get(:@http)
|
79
|
+
end
|
80
|
+
|
81
|
+
[:response_parser, :headers, :request].each do |m|
|
82
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|