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.
Files changed (65) 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 +96 -19
  11. data/lib/llm/providers/anthropic/error_handler.rb +2 -0
  12. data/lib/llm/providers/anthropic/format.rb +2 -0
  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 +12 -6
  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 +13 -5
  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 +21 -8
  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 +1 -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 +101 -61
  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 +1 -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 -49
  63. data/LICENSE.txt +0 -21
  64. data/lib/llm/conversation.rb +0 -90
  65. data/lib/llm/message_queue.rb +0 -54
@@ -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,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 URI === content
28
- [{type: :image_url, image_url: {url: content.to_s}}]
29
- else
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
- 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
@@ -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