llm.rb 0.2.0 → 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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +264 -110
  3. data/lib/llm/buffer.rb +83 -0
  4. data/lib/llm/chat.rb +131 -0
  5. data/lib/llm/file.rb +26 -40
  6. data/lib/llm/http_client.rb +10 -5
  7. data/lib/llm/message.rb +14 -8
  8. data/lib/llm/mime.rb +54 -0
  9. data/lib/llm/multipart.rb +98 -0
  10. data/lib/llm/provider.rb +116 -12
  11. data/lib/llm/providers/anthropic/error_handler.rb +2 -0
  12. data/lib/llm/providers/anthropic/format.rb +9 -1
  13. data/lib/llm/providers/anthropic/response_parser.rb +3 -1
  14. data/lib/llm/providers/anthropic.rb +14 -5
  15. data/lib/llm/providers/gemini/audio.rb +77 -0
  16. data/lib/llm/providers/gemini/error_handler.rb +2 -0
  17. data/lib/llm/providers/gemini/files.rb +160 -0
  18. data/lib/llm/providers/gemini/format.rb +19 -7
  19. data/lib/llm/providers/gemini/images.rb +99 -0
  20. data/lib/llm/providers/gemini/response_parser.rb +27 -1
  21. data/lib/llm/providers/gemini.rb +62 -6
  22. data/lib/llm/providers/ollama/error_handler.rb +2 -0
  23. data/lib/llm/providers/ollama/format.rb +18 -4
  24. data/lib/llm/providers/ollama/response_parser.rb +3 -1
  25. data/lib/llm/providers/ollama.rb +30 -7
  26. data/lib/llm/providers/openai/audio.rb +97 -0
  27. data/lib/llm/providers/openai/error_handler.rb +2 -0
  28. data/lib/llm/providers/openai/files.rb +148 -0
  29. data/lib/llm/providers/openai/format.rb +26 -7
  30. data/lib/llm/providers/openai/images.rb +109 -0
  31. data/lib/llm/providers/openai/response_parser.rb +58 -5
  32. data/lib/llm/providers/openai/responses.rb +78 -0
  33. data/lib/llm/providers/openai.rb +52 -6
  34. data/lib/llm/providers/voyageai.rb +2 -2
  35. data/lib/llm/response/audio.rb +13 -0
  36. data/lib/llm/response/audio_transcription.rb +14 -0
  37. data/lib/llm/response/audio_translation.rb +14 -0
  38. data/lib/llm/response/download_file.rb +15 -0
  39. data/lib/llm/response/file.rb +42 -0
  40. data/lib/llm/response/filelist.rb +18 -0
  41. data/lib/llm/response/image.rb +29 -0
  42. data/lib/llm/response/output.rb +56 -0
  43. data/lib/llm/response.rb +18 -6
  44. data/lib/llm/utils.rb +19 -0
  45. data/lib/llm/version.rb +1 -1
  46. data/lib/llm.rb +5 -2
  47. data/llm.gemspec +1 -6
  48. data/spec/anthropic/completion_spec.rb +1 -1
  49. data/spec/gemini/completion_spec.rb +22 -1
  50. data/spec/gemini/conversation_spec.rb +31 -0
  51. data/spec/gemini/files_spec.rb +124 -0
  52. data/spec/gemini/images_spec.rb +47 -0
  53. data/spec/llm/conversation_spec.rb +133 -1
  54. data/spec/ollama/completion_spec.rb +1 -1
  55. data/spec/ollama/conversation_spec.rb +31 -0
  56. data/spec/openai/audio_spec.rb +55 -0
  57. data/spec/openai/completion_spec.rb +22 -1
  58. data/spec/openai/files_spec.rb +150 -0
  59. data/spec/openai/images_spec.rb +95 -0
  60. data/spec/openai/responses_spec.rb +51 -0
  61. data/spec/setup.rb +8 -0
  62. metadata +31 -51
  63. data/LICENSE.txt +0 -21
  64. data/lib/llm/conversation.rb +0 -50
  65. data/lib/llm/lazy_conversation.rb +0 -51
  66. data/lib/llm/message_queue.rb +0 -47
  67. data/spec/llm/lazy_conversation_spec.rb +0 -92
@@ -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::HTTPClient#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::HTTPClient#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 = 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::HTTPClient#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 = 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]
@@ -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,13 +1,23 @@
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)
10
- messages.map { {role: _1.role, content: format_content(_1.content)} }
13
+ def format(messages, mode)
14
+ messages.map do
15
+ if Hash === _1
16
+ {role: _1[:role], content: format_content(_1[:content], mode)}
17
+ else
18
+ {role: _1.role, content: format_content(_1.content, mode)}
19
+ end
20
+ end
11
21
  end
12
22
 
13
23
  private
@@ -17,11 +27,20 @@ class LLM::OpenAI
17
27
  # The content to format
18
28
  # @return [String, Hash]
19
29
  # The formatted content
20
- def format_content(content)
21
- if URI === content
22
- [{type: :image_url, image_url: {url: content.to_s}}]
23
- else
24
- 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
25
44
  end
26
45
  end
27
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
- mesg = _1["message"]
27
- logprobs = _1["logprobs"]
28
- role, content = mesg.values_at("role", "content")
29
- LLM::Message.new(role, content, {completion: self, logprobs:})
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
@@ -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/format"
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: "text-embedding-3-small"}.merge!(params))
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: "gpt-4o-mini"}.merge!(params)
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: "voyage-2"}.merge!(params))
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