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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +318 -110
  3. data/lib/llm/buffer.rb +83 -0
  4. data/lib/llm/chat.rb +131 -0
  5. data/lib/llm/error.rb +3 -3
  6. data/lib/llm/file.rb +36 -40
  7. data/lib/llm/message.rb +21 -8
  8. data/lib/llm/mime.rb +54 -0
  9. data/lib/llm/multipart.rb +100 -0
  10. data/lib/llm/provider.rb +123 -21
  11. data/lib/llm/providers/anthropic/error_handler.rb +3 -1
  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 +4 -2
  17. data/lib/llm/providers/gemini/files.rb +162 -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 +3 -1
  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 +3 -1
  28. data/lib/llm/providers/openai/files.rb +148 -0
  29. data/lib/llm/providers/openai/format.rb +22 -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 +85 -0
  33. data/lib/llm/providers/openai.rb +52 -6
  34. data/lib/llm/providers/voyageai/error_handler.rb +1 -1
  35. data/lib/llm/providers/voyageai.rb +2 -2
  36. data/lib/llm/response/audio.rb +13 -0
  37. data/lib/llm/response/audio_transcription.rb +14 -0
  38. data/lib/llm/response/audio_translation.rb +14 -0
  39. data/lib/llm/response/download_file.rb +15 -0
  40. data/lib/llm/response/file.rb +42 -0
  41. data/lib/llm/response/filelist.rb +18 -0
  42. data/lib/llm/response/image.rb +29 -0
  43. data/lib/llm/response/output.rb +56 -0
  44. data/lib/llm/response.rb +18 -6
  45. data/lib/llm/utils.rb +19 -0
  46. data/lib/llm/version.rb +1 -1
  47. data/lib/llm.rb +5 -2
  48. data/llm.gemspec +1 -6
  49. data/spec/anthropic/completion_spec.rb +1 -1
  50. data/spec/gemini/completion_spec.rb +1 -1
  51. data/spec/gemini/conversation_spec.rb +31 -0
  52. data/spec/gemini/files_spec.rb +124 -0
  53. data/spec/gemini/images_spec.rb +47 -0
  54. data/spec/llm/conversation_spec.rb +107 -62
  55. data/spec/ollama/completion_spec.rb +1 -1
  56. data/spec/ollama/conversation_spec.rb +31 -0
  57. data/spec/openai/audio_spec.rb +55 -0
  58. data/spec/openai/completion_spec.rb +5 -4
  59. data/spec/openai/files_spec.rb +204 -0
  60. data/spec/openai/images_spec.rb +95 -0
  61. data/spec/openai/responses_spec.rb +51 -0
  62. data/spec/setup.rb +8 -0
  63. metadata +31 -50
  64. data/LICENSE.txt +0 -21
  65. data/lib/llm/conversation.rb +0 -90
  66. data/lib/llm/http_client.rb +0 -29
  67. data/lib/llm/message_queue.rb +0 -54
@@ -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::Provider#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::Provider#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
@@ -25,7 +25,7 @@ class LLM::VoyageAI
25
25
  when Net::HTTPTooManyRequests
26
26
  raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
27
27
  else
28
- raise LLM::Error::BadResponse.new { _1.response = res }, "Unexpected response"
28
+ raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
29
29
  end
30
30
  end
31
31
  end
@@ -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
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # The {LLM::Response::FileList LLM::Response::FileList} class represents a
6
+ # list of file objects that are returned by a provider. It is an Enumerable
7
+ # object, and can be used to iterate over the file objects in a way that is
8
+ # similar to an array. Each element is an instance of OpenStruct.
9
+ class Response::FileList < Response
10
+ include Enumerable
11
+
12
+ attr_accessor :files
13
+
14
+ def each(&)
15
+ @files.each(&)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # The {LLM::Response::Image LLM::Response::Image} class represents
6
+ # an image response. An image response might encapsulate one or more
7
+ # URLs, or a base64 encoded image -- depending on the provider.
8
+ class Response::Image < Response
9
+ ##
10
+ # Returns one or more image objects, or nil
11
+ # @return [Array<OpenStruct>, nil]
12
+ def images
13
+ parsed[:images].any? ? parsed[:images] : nil
14
+ end
15
+
16
+ ##
17
+ # Returns one or more image URLs, or nil
18
+ # @return [Array<String>, nil]
19
+ def urls
20
+ parsed[:urls].any? ? parsed[:urls] : nil
21
+ end
22
+
23
+ private
24
+
25
+ def parsed
26
+ @parsed ||= parse_image(body)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ class Response::Output < Response
5
+ ##
6
+ # @return [String]
7
+ # Returns the id of the response
8
+ def id
9
+ parsed[:id]
10
+ end
11
+
12
+ ##
13
+ # @return [String]
14
+ # Returns the model name
15
+ def model
16
+ parsed[:model]
17
+ end
18
+
19
+ ##
20
+ # @return [Array<LLM::Message>]
21
+ def outputs
22
+ parsed[:outputs]
23
+ end
24
+
25
+ ##
26
+ # @return [Integer]
27
+ # Returns the input token count
28
+ def input_tokens
29
+ parsed[:input_tokens]
30
+ end
31
+
32
+ ##
33
+ # @return [Integer]
34
+ # Returns the output token count
35
+ def output_tokens
36
+ parsed[:output_tokens]
37
+ end
38
+
39
+ ##
40
+ # @return [Integer]
41
+ # Returns the total count of tokens
42
+ def total_tokens
43
+ parsed[:total_tokens]
44
+ end
45
+
46
+ private
47
+
48
+ ##
49
+ # @private
50
+ # @return [Hash]
51
+ # Returns the parsed response from the provider
52
+ def parsed
53
+ @parsed ||= parse_output_response(body)
54
+ end
55
+ end
56
+ end
data/lib/llm/response.rb CHANGED
@@ -5,11 +5,14 @@ module LLM
5
5
  require "json"
6
6
  require_relative "response/completion"
7
7
  require_relative "response/embedding"
8
-
9
- ##
10
- # @return [Hash]
11
- # Returns the response body
12
- attr_reader :body
8
+ require_relative "response/output"
9
+ require_relative "response/image"
10
+ require_relative "response/audio"
11
+ require_relative "response/audio_transcription"
12
+ require_relative "response/audio_translation"
13
+ require_relative "response/file"
14
+ require_relative "response/filelist"
15
+ require_relative "response/download_file"
13
16
 
14
17
  ##
15
18
  # @param [Net::HTTPResponse] res
@@ -18,7 +21,16 @@ module LLM
18
21
  # Returns an instance of LLM::Response
19
22
  def initialize(res)
20
23
  @res = res
21
- @body = JSON.parse(res.body)
24
+ end
25
+
26
+ ##
27
+ # Returns the response body
28
+ # @return [Hash, String]
29
+ def body
30
+ @body ||= case @res["content-type"]
31
+ when %r|\Aapplication/json\s*| then JSON.parse(@res.body)
32
+ else @res.body
33
+ end
22
34
  end
23
35
  end
24
36
  end
data/lib/llm/utils.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # @private
5
+ module LLM::Utils
6
+ def camelcase(key)
7
+ key.to_s
8
+ .split("_")
9
+ .map.with_index { (_2 > 0) ? _1.capitalize : _1 }
10
+ .join
11
+ end
12
+
13
+ def snakecase(key)
14
+ key
15
+ .split(/([A-Z])/)
16
+ .map { (_1.size == 1) ? "_#{_1.downcase}" : _1 }
17
+ .join
18
+ end
19
+ end
data/lib/llm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LLM
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/llm.rb CHANGED
@@ -2,14 +2,17 @@
2
2
 
3
3
  module LLM
4
4
  require_relative "llm/version"
5
+ require_relative "llm/utils"
5
6
  require_relative "llm/error"
6
7
  require_relative "llm/message"
7
8
  require_relative "llm/response"
9
+ require_relative "llm/mime"
10
+ require_relative "llm/multipart"
8
11
  require_relative "llm/file"
9
12
  require_relative "llm/model"
10
13
  require_relative "llm/provider"
11
- require_relative "llm/conversation"
12
- require_relative "llm/message_queue"
14
+ require_relative "llm/chat"
15
+ require_relative "llm/buffer"
13
16
  require_relative "llm/core_ext/ostruct"
14
17
 
15
18
  module_function
data/llm.gemspec CHANGED
@@ -14,12 +14,11 @@ Gem::Specification.new do |spec|
14
14
  "flexible, and easy to use."
15
15
  spec.description = spec.summary
16
16
  spec.homepage = "https://github.com/llmrb/llm"
17
- spec.license = "MIT"
17
+ spec.license = "0BSDL"
18
18
  spec.required_ruby_version = ">= 3.0.0"
19
19
 
20
20
  spec.metadata["homepage_uri"] = spec.homepage
21
21
  spec.metadata["source_code_uri"] = "https://github.com/llmrb/llm"
22
- spec.metadata["changelog_uri"] = "https://github.com/llmrb/llm/blob/main/CHANGELOG.md"
23
22
 
24
23
  spec.files = Dir[
25
24
  "README.md", "LICENSE.txt",
@@ -29,10 +28,6 @@ Gem::Specification.new do |spec|
29
28
  ]
30
29
  spec.require_paths = ["lib"]
31
30
 
32
- spec.add_runtime_dependency "net-http", "~> 0.6.0"
33
- spec.add_runtime_dependency "json"
34
- spec.add_runtime_dependency "yaml"
35
-
36
31
  spec.add_development_dependency "webmock", "~> 3.24.0"
37
32
  spec.add_development_dependency "yard", "~> 0.9.37"
38
33
  spec.add_development_dependency "kramdown", "~> 2.4"
@@ -37,7 +37,7 @@ RSpec.describe "LLM::Anthropic: completions" do
37
37
  end
38
38
 
39
39
  it "includes the response" do
40
- expect(choice.extra[:completion]).to eq(response)
40
+ expect(choice.extra[:response]).to eq(response)
41
41
  end
42
42
  end
43
43
  end
@@ -41,7 +41,7 @@ RSpec.describe "LLM::Gemini: completions" do
41
41
  end
42
42
 
43
43
  it "includes the response" do
44
- expect(choice.extra[:completion]).to eq(response)
44
+ expect(choice.extra[:response]).to eq(response)
45
45
  end
46
46
  end
47
47
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::Chat: gemini" do
6
+ let(:described_class) { LLM::Chat }
7
+ let(:provider) { LLM.gemini(token) }
8
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
9
+ let(:conversation) { described_class.new(provider, **params).lazy }
10
+
11
+ context "when asked to describe an image",
12
+ vcr: {cassette_name: "gemini/conversations/multimodal_response"} do
13
+ subject { conversation.last_message }
14
+
15
+ let(:params) { {} }
16
+ let(:image) { LLM::File("spec/fixtures/images/bluebook.png") }
17
+
18
+ before do
19
+ conversation.chat(image, :user)
20
+ conversation.chat("Describe the image with a short sentance", :user)
21
+ end
22
+
23
+ it "describes the image" do
24
+ is_expected.to have_attributes(
25
+ role: "model",
26
+ content: "That's a simple illustration of a book " \
27
+ "resting on a blue, X-shaped book stand.\n"
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::Gemini::Files" do
6
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
+ let(:provider) { LLM.gemini(token) }
8
+
9
+ context "when given a successful create operation (bismillah.mp3)",
10
+ vcr: {cassette_name: "gemini/files/successful_create_bismillah"} do
11
+ subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
12
+ after { provider.files.delete(file:) }
13
+
14
+ it "is successful" do
15
+ expect(file).to be_instance_of(LLM::Response::File)
16
+ end
17
+
18
+ it "returns a file object" do
19
+ expect(file).to have_attributes(
20
+ name: instance_of(String),
21
+ display_name: "bismillah.mp3"
22
+ )
23
+ end
24
+ end
25
+
26
+ context "when given a successful delete operation (bismillah.mp3)",
27
+ vcr: {cassette_name: "gemini/files/successful_delete_bismillah"} do
28
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
29
+ subject { provider.files.delete(file:) }
30
+
31
+ it "is successful" do
32
+ is_expected.to be_instance_of(Net::HTTPOK)
33
+ end
34
+ end
35
+
36
+ context "when given a successful get operation (bismillah.mp3)",
37
+ vcr: {cassette_name: "gemini/files/successful_get_bismillah"} do
38
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
39
+ subject { provider.files.get(file:) }
40
+ after { provider.files.delete(file:) }
41
+
42
+ it "is successful" do
43
+ is_expected.to be_instance_of(LLM::Response::File)
44
+ end
45
+
46
+ it "returns a file object" do
47
+ is_expected.to have_attributes(
48
+ name: instance_of(String),
49
+ display_name: "bismillah.mp3"
50
+ )
51
+ end
52
+ end
53
+
54
+ context "when given a successful translation operation (bismillah.mp3)",
55
+ vcr: {cassette_name: "gemini/files/successful_translation_bismillah"} do
56
+ subject { bot.last_message.content }
57
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")) }
58
+ let(:bot) { LLM::Chat.new(provider).lazy }
59
+ after { provider.files.delete(file:) }
60
+
61
+ before do
62
+ bot.chat file
63
+ bot.chat "Translate the contents of the audio file into English"
64
+ bot.chat "The audio is referenced in the first message I sent to you"
65
+ bot.chat "Provide no other content except the translation"
66
+ end
67
+
68
+ it "translates the audio clip" do
69
+ is_expected.to eq("In the name of Allah, the Most Gracious, the Most Merciful.\n")
70
+ end
71
+ end
72
+
73
+ context "when given a successful translation operation (alhamdullilah.mp3)",
74
+ vcr: {cassette_name: "gemini/files/successful_translation_alhamdullilah"} do
75
+ subject { bot.last_message.content }
76
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/audio/alhamdullilah.mp3")) }
77
+ let(:bot) { LLM::Chat.new(provider).lazy }
78
+ after { provider.files.delete(file:) }
79
+
80
+ before do
81
+ bot.chat [
82
+ "Translate the contents of the audio file into English",
83
+ "Provide no other content except the translation",
84
+ file
85
+ ]
86
+ end
87
+
88
+ it "translates the audio clip" do
89
+ is_expected.to eq("All praise is due to Allah, Lord of the worlds.\n")
90
+ end
91
+ end
92
+
93
+ context "when given a successful all operation",
94
+ vcr: {cassette_name: "gemini/files/successful_all"} do
95
+ let!(:files) do
96
+ [
97
+ provider.files.create(file: LLM::File("spec/fixtures/audio/bismillah.mp3")),
98
+ provider.files.create(file: LLM::File("spec/fixtures/audio/alhamdullilah.mp3"))
99
+ ]
100
+ end
101
+
102
+ subject(:response) { provider.files.all }
103
+ after { files.each { |file| provider.files.delete(file:) } }
104
+
105
+ it "is successful" do
106
+ expect(response).to be_instance_of(LLM::Response::FileList)
107
+ end
108
+
109
+ it "returns an array of file objects" do
110
+ expect(response).to match_array(
111
+ [
112
+ have_attributes(
113
+ name: instance_of(String),
114
+ display_name: "bismillah.mp3"
115
+ ),
116
+ have_attributes(
117
+ name: instance_of(String),
118
+ display_name: "alhamdullilah.mp3"
119
+ )
120
+ ]
121
+ )
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::Gemini::Images" do
6
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
+ let(:provider) { LLM.gemini(token) }
8
+
9
+ context "when given a successful create operation",
10
+ vcr: {cassette_name: "gemini/images/successful_create"} do
11
+ subject(:response) { provider.images.create(prompt: "A dog on a rocket to the moon") }
12
+
13
+ it "is successful" do
14
+ expect(response).to be_instance_of(LLM::Response::Image)
15
+ end
16
+
17
+ it "returns an encoded string" do
18
+ expect(response.images[0].encoded).to be_instance_of(String)
19
+ end
20
+
21
+ it "returns a binary string" do
22
+ expect(response.images[0].binary).to be_instance_of(String)
23
+ end
24
+ end
25
+
26
+ context "when given a successful edit operation",
27
+ vcr: {cassette_name: "gemini/images/successful_edit"} do
28
+ subject(:response) do
29
+ provider.images.edit(
30
+ image: LLM::File("spec/fixtures/images/bluebook.png"),
31
+ prompt: "Book is floating in the clouds"
32
+ )
33
+ end
34
+
35
+ it "is successful" do
36
+ expect(response).to be_instance_of(LLM::Response::Image)
37
+ end
38
+
39
+ it "returns data" do
40
+ expect(response.images[0].encoded).to be_instance_of(String)
41
+ end
42
+
43
+ it "returns a url" do
44
+ expect(response.images[0].binary).to be_instance_of(String)
45
+ end
46
+ end
47
+ end