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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "setup"
4
4
 
5
- RSpec.describe "LLM::Conversation: non-lazy" do
5
+ RSpec.describe "LLM::Chat: non-lazy" do
6
6
  shared_examples "a multi-turn conversation" do
7
7
  context "when given a thread of messages" do
8
8
  let(:inputs) do
@@ -57,91 +57,136 @@ RSpec.describe "LLM::Conversation: non-lazy" do
57
57
  end
58
58
  end
59
59
 
60
- RSpec.describe "LLM::Conversation: lazy" do
61
- let(:described_class) { LLM::Conversation }
60
+ RSpec.describe "LLM::Chat: lazy" do
61
+ let(:described_class) { LLM::Chat }
62
62
  let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
63
- let(:prompt) { "Keep your answers short and concise, and provide three answers to the three questions" }
63
+ let(:prompt) do
64
+ "Keep your answers short and concise, and provide three answers to the three questions" \
65
+ "There should be one answer per line" \
66
+ "An answer should be a number, for example: 5" \
67
+ "Nothing else"
68
+ end
64
69
 
65
- context "with gemini",
66
- vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
67
- let(:provider) { LLM.gemini(token) }
68
- let(:conversation) { described_class.new(provider).lazy }
70
+ context "when given completions" do
71
+ context "with gemini",
72
+ vcr: {cassette_name: "gemini/lazy_conversation/successful_response"} do
73
+ let(:provider) { LLM.gemini(token) }
74
+ let(:conversation) { described_class.new(provider).lazy }
69
75
 
70
- context "when given a thread of messages" do
71
- subject(:message) { conversation.messages.to_a[-1] }
76
+ context "when given a thread of messages" do
77
+ subject(:message) { conversation.messages.to_a[-1] }
72
78
 
73
- before do
74
- conversation.chat prompt
75
- conversation.chat "What is 3+2 ?"
76
- conversation.chat "What is 5+5 ?"
77
- conversation.chat "What is 5+7 ?"
78
- end
79
+ before do
80
+ conversation.chat prompt
81
+ conversation.chat "What is 3+2 ?"
82
+ conversation.chat "What is 5+5 ?"
83
+ conversation.chat "What is 5+7 ?"
84
+ end
79
85
 
80
- it "maintains a conversation" do
81
- is_expected.to have_attributes(
82
- role: "model",
83
- content: "5\n10\n12\n"
84
- )
86
+ it "maintains a conversation" do
87
+ is_expected.to have_attributes(
88
+ role: "model",
89
+ content: "5\n10\n12\n"
90
+ )
91
+ end
85
92
  end
86
93
  end
87
- end
88
94
 
89
- context "with openai" do
90
- let(:provider) { LLM.openai(token) }
91
- let(:conversation) { described_class.new(provider).lazy }
95
+ context "with openai" do
96
+ let(:provider) { LLM.openai(token) }
97
+ let(:conversation) { described_class.new(provider).lazy }
98
+
99
+ context "when given a thread of messages",
100
+ vcr: {cassette_name: "openai/lazy_conversation/completions/successful_response"} do
101
+ subject(:message) { conversation.recent_message }
92
102
 
93
- context "when given a thread of messages",
94
- vcr: {cassette_name: "openai/lazy_conversation/successful_response"} do
95
- subject(:message) { conversation.recent_message }
103
+ before do
104
+ conversation.chat prompt, :system
105
+ conversation.chat "What is 3+2 ?"
106
+ conversation.chat "What is 5+5 ?"
107
+ conversation.chat "What is 5+7 ?"
108
+ end
96
109
 
97
- before do
98
- conversation.chat prompt, :system
99
- conversation.chat "What is 3+2 ?"
100
- conversation.chat "What is 5+5 ?"
101
- conversation.chat "What is 5+7 ?"
110
+ it "maintains a conversation" do
111
+ is_expected.to have_attributes(
112
+ role: "assistant",
113
+ content: %r|5\s*\n10\s*\n12\s*|
114
+ )
115
+ end
102
116
  end
103
117
 
104
- it "maintains a conversation" do
105
- is_expected.to have_attributes(
106
- role: "assistant",
107
- content: "1. 5 \n2. 10 \n3. 12 "
108
- )
118
+ context "when given a specific model",
119
+ vcr: {cassette_name: "openai/lazy_conversation/completions/successful_response_o3_mini"} do
120
+ let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
121
+
122
+ it "maintains the model throughout a conversation" do
123
+ conversation.chat(prompt, :system)
124
+ expect(conversation.recent_message.extra[:response].model).to eq("o3-mini-2025-01-31")
125
+ conversation.chat("What is 5+5?")
126
+ expect(conversation.recent_message.extra[:response].model).to eq("o3-mini-2025-01-31")
127
+ end
109
128
  end
110
129
  end
111
130
 
112
- context "when given a specific model",
113
- vcr: {cassette_name: "openai/lazy_conversation/successful_response_o3_mini"} do
114
- let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
131
+ context "with ollama",
132
+ vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
133
+ let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
134
+ let(:conversation) { described_class.new(provider).lazy }
135
+
136
+ context "when given a thread of messages" do
137
+ subject(:message) { conversation.recent_message }
138
+
139
+ before do
140
+ conversation.chat prompt, :system
141
+ conversation.chat "What is 3+2 ?"
142
+ conversation.chat "What is 5+5 ?"
143
+ conversation.chat "What is 5+7 ?"
144
+ end
115
145
 
116
- it "maintains the model throughout a conversation" do
117
- conversation.chat(prompt, :system)
118
- expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
119
- conversation.chat("What is 5+5?")
120
- expect(conversation.recent_message.extra[:completion].model).to eq("o3-mini-2025-01-31")
146
+ it "maintains a conversation" do
147
+ is_expected.to have_attributes(
148
+ role: "assistant",
149
+ content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
150
+ )
151
+ end
121
152
  end
122
153
  end
123
154
  end
124
155
 
125
- context "with ollama",
126
- vcr: {cassette_name: "ollama/lazy_conversation/successful_response"} do
127
- let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
128
- let(:conversation) { described_class.new(provider).lazy }
156
+ context "when given responses" do
157
+ context "with openai" do
158
+ let(:provider) { LLM.openai(token) }
159
+ let(:conversation) { described_class.new(provider).lazy }
129
160
 
130
- context "when given a thread of messages" do
131
- subject(:message) { conversation.recent_message }
161
+ context "when given a thread of messages",
162
+ vcr: {cassette_name: "openai/lazy_conversation/responses/successful_response"} do
163
+ subject(:message) { conversation.recent_message }
132
164
 
133
- before do
134
- conversation.chat prompt, :system
135
- conversation.chat "What is 3+2 ?"
136
- conversation.chat "What is 5+5 ?"
137
- conversation.chat "What is 5+7 ?"
165
+ before do
166
+ conversation.respond prompt, :developer
167
+ conversation.respond "What is 3+2 ?"
168
+ conversation.respond "What is 5+5 ?"
169
+ conversation.respond "What is 5+7 ?"
170
+ end
171
+
172
+ it "maintains a conversation" do
173
+ is_expected.to have_attributes(
174
+ role: "assistant",
175
+ content: %r|5\s*\n10\s*\n12\s*|
176
+ )
177
+ end
138
178
  end
139
179
 
140
- it "maintains a conversation" do
141
- is_expected.to have_attributes(
142
- role: "assistant",
143
- content: "Here are the calculations:\n\n1. 3 + 2 = 5\n2. 5 + 5 = 10\n3. 5 + 7 = 12"
144
- )
180
+ context "when given a specific model",
181
+ vcr: {cassette_name: "openai/lazy_conversation/responses/successful_response_o3_mini"} do
182
+ let(:conversation) { described_class.new(provider, model: provider.models["o3-mini"]).lazy }
183
+
184
+ it "maintains the model throughout a conversation" do
185
+ conversation.respond(prompt, :developer)
186
+ expect(conversation.last_message.extra[:response].model).to eq("o3-mini-2025-01-31")
187
+ conversation.respond("What is 5+5?")
188
+ expect(conversation.last_message.extra[:response].model).to eq("o3-mini-2025-01-31")
189
+ end
145
190
  end
146
191
  end
147
192
  end
@@ -36,7 +36,7 @@ RSpec.describe "LLM::Ollama: completions" do
36
36
  end
37
37
 
38
38
  it "includes the response" do
39
- expect(choice.extra[:completion]).to eq(response)
39
+ expect(choice.extra[:response]).to eq(response)
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::Chat: ollama" do
6
+ let(:described_class) { LLM::Chat }
7
+ let(:provider) { LLM.ollama(nil, host: "eel.home.network") }
8
+ let(:conversation) { described_class.new(provider, **params).lazy }
9
+
10
+ context "when asked to describe an image",
11
+ vcr: {cassette_name: "ollama/conversations/multimodal_response"} do
12
+ subject { conversation.last_message }
13
+
14
+ let(:params) { {model: "llava"} }
15
+ let(:image) { LLM::File("spec/fixtures/images/bluebook.png") }
16
+
17
+ before do
18
+ conversation.chat(image, :user)
19
+ conversation.chat("Describe the image with a short sentance", :user)
20
+ end
21
+
22
+ it "describes the image" do
23
+ is_expected.to have_attributes(
24
+ role: "assistant",
25
+ content: " The image is a graphic illustration of a book" \
26
+ " with its pages spread out, symbolizing openness" \
27
+ " or knowledge. "
28
+ )
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::OpenAI::Audio" do
6
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
+ let(:provider) { LLM.openai(token) }
8
+
9
+ context "when given a successful create operation",
10
+ vcr: {cassette_name: "openai/audio/successful_create"} do
11
+ subject(:response) { provider.audio.create_speech(input: "A dog on a rocket to the moon") }
12
+
13
+ it "is successful" do
14
+ expect(response).to be_instance_of(LLM::Response::Audio)
15
+ end
16
+
17
+ it "returns an audio" do
18
+ expect(response.audio).to be_instance_of(StringIO)
19
+ end
20
+ end
21
+
22
+ context "when given a successful transcription operation",
23
+ vcr: {cassette_name: "openai/audio/successful_transcription"} do
24
+ subject(:response) do
25
+ provider.audio.create_transcription(
26
+ file: LLM::File("spec/fixtures/audio/rocket.mp3")
27
+ )
28
+ end
29
+
30
+ it "is successful" do
31
+ expect(response).to be_instance_of(LLM::Response::AudioTranscription)
32
+ end
33
+
34
+ it "returns a transcription" do
35
+ expect(response.text).to eq("A dog on a rocket to the moon.")
36
+ end
37
+ end
38
+
39
+ context "when given a successful translation operation",
40
+ vcr: {cassette_name: "openai/audio/successful_translation"} do
41
+ subject(:response) do
42
+ provider.audio.create_translation(
43
+ file: LLM::File("spec/fixtures/audio/bismillah.mp3")
44
+ )
45
+ end
46
+
47
+ it "is successful" do
48
+ expect(response).to be_instance_of(LLM::Response::AudioTranslation)
49
+ end
50
+
51
+ it "returns a translation (Arabic => English)" do
52
+ expect(response.text).to eq("In the name of Allah, the Beneficent, the Merciful.")
53
+ end
54
+ end
55
+ end
@@ -37,7 +37,7 @@ RSpec.describe "LLM::OpenAI: 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
@@ -47,7 +47,8 @@ RSpec.describe "LLM::OpenAI: completions" do
47
47
  subject(:response) do
48
48
  openai.complete "What is your name? What age are you?", :user, messages: [
49
49
  {role: "system", content: "Answer all of my questions"},
50
- {role: "system", content: "Your name is Pablo, you are 25 years old and you are my amigo"}
50
+ {role: "system", content: "Answer in the format: My name is <name> and I am <age> years old"},
51
+ {role: "system", content: "Your name is Pablo and you are 25 years old"},
51
52
  ]
52
53
  end
53
54
 
@@ -56,7 +57,7 @@ RSpec.describe "LLM::OpenAI: completions" do
56
57
  choices: [
57
58
  have_attributes(
58
59
  role: "assistant",
59
- content: "My name is Pablo, and I'm 25 years old! How can I help you today, amigo?"
60
+ content: %r|\AMy name is Pablo and I am 25 years old|
60
61
  )
61
62
  ]
62
63
  )
@@ -68,7 +69,7 @@ RSpec.describe "LLM::OpenAI: completions" do
68
69
  subject(:response) { openai.complete(URI("/foobar.exe"), :user) }
69
70
 
70
71
  it "raises an error" do
71
- expect { response }.to raise_error(LLM::Error::BadResponse)
72
+ expect { response }.to raise_error(LLM::Error::ResponseError)
72
73
  end
73
74
 
74
75
  it "includes the response" do
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::OpenAI::Files" do
6
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
+ let(:provider) { LLM.openai(token) }
8
+
9
+ context "when given a successful create operation (haiku1.txt)",
10
+ vcr: {cassette_name: "openai/files/successful_create_haiku1"} do
11
+ subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku1.txt")) }
12
+
13
+ it "is successful" do
14
+ expect(file).to be_instance_of(LLM::Response::File)
15
+ ensure
16
+ provider.files.delete(file:)
17
+ end
18
+
19
+ it "returns a file object" do
20
+ expect(file).to have_attributes(
21
+ id: instance_of(String),
22
+ filename: "haiku1.txt",
23
+ purpose: "assistants"
24
+ )
25
+ ensure
26
+ provider.files.delete(file:)
27
+ end
28
+ end
29
+
30
+ context "when given a successful create operation (haiku2.txt)",
31
+ vcr: {cassette_name: "openai/files/successful_create_haiku2"} do
32
+ subject(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku2.txt")) }
33
+
34
+ it "is successful" do
35
+ expect(file).to be_instance_of(LLM::Response::File)
36
+ ensure
37
+ provider.files.delete(file:)
38
+ end
39
+
40
+ it "returns a file object" do
41
+ expect(file).to have_attributes(
42
+ id: instance_of(String),
43
+ filename: "haiku2.txt",
44
+ purpose: "assistants"
45
+ )
46
+ ensure
47
+ provider.files.delete(file:)
48
+ end
49
+ end
50
+
51
+ context "when given a successful delete operation (haiku3.txt)",
52
+ vcr: {cassette_name: "openai/files/successful_delete_haiku3"} do
53
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku3.txt")) }
54
+ subject { provider.files.delete(file:) }
55
+
56
+ it "is successful" do
57
+ is_expected.to be_instance_of(OpenStruct)
58
+ end
59
+
60
+ it "returns deleted status" do
61
+ is_expected.to have_attributes(
62
+ deleted: true
63
+ )
64
+ end
65
+ end
66
+
67
+ context "when given a successful get operation (haiku4.txt)",
68
+ vcr: {cassette_name: "openai/files/successful_get_haiku4"} do
69
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/haiku4.txt")) }
70
+ subject { provider.files.get(file:) }
71
+
72
+ it "is successful" do
73
+ is_expected.to be_instance_of(LLM::Response::File)
74
+ ensure
75
+ provider.files.delete(file:)
76
+ end
77
+
78
+ it "returns a file object" do
79
+ is_expected.to have_attributes(
80
+ id: instance_of(String),
81
+ filename: "haiku4.txt",
82
+ purpose: "assistants"
83
+ )
84
+ ensure
85
+ provider.files.delete(file:)
86
+ end
87
+ end
88
+
89
+ context "when given a successful all operation",
90
+ vcr: {cassette_name: "openai/files/successful_all"} do
91
+ let!(:files) do
92
+ [
93
+ provider.files.create(file: LLM::File("spec/fixtures/documents/haiku1.txt")),
94
+ provider.files.create(file: LLM::File("spec/fixtures/documents/haiku2.txt"))
95
+ ]
96
+ end
97
+ subject(:file) { provider.files.all }
98
+
99
+ it "is successful" do
100
+ expect(file).to be_instance_of(LLM::Response::FileList)
101
+ ensure
102
+ files.each { |file| provider.files.delete(file:) }
103
+ end
104
+
105
+ it "returns an array of file objects" do
106
+ expect(file).to match_array(
107
+ [
108
+ have_attributes(
109
+ id: instance_of(String),
110
+ filename: "haiku1.txt",
111
+ purpose: "assistants"
112
+ ),
113
+ have_attributes(
114
+ id: instance_of(String),
115
+ filename: "haiku2.txt",
116
+ purpose: "assistants"
117
+ )
118
+ ]
119
+ )
120
+ ensure
121
+ files.each { |file| provider.files.delete(file:) }
122
+ end
123
+ end
124
+
125
+ context "when asked to describe the contents of a file",
126
+ vcr: {cassette_name: "openai/files/describe_freebsd.sysctl.pdf"} do
127
+ subject { bot.last_message.content.downcase[0..2] }
128
+ let(:bot) { LLM::Chat.new(provider).lazy }
129
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
130
+
131
+ before do
132
+ bot.respond(file)
133
+ bot.respond("Is this PDF document about FreeBSD?")
134
+ bot.respond("Answer with yes or no. Nothing else.")
135
+ end
136
+
137
+ it "describes the document" do
138
+ is_expected.to eq("yes")
139
+ ensure
140
+ provider.files.delete(file:)
141
+ end
142
+ end
143
+
144
+ context "when asked to describe the contents of a file",
145
+ vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_2.pdf"} do
146
+ subject { bot.last_message.content.downcase[0..2] }
147
+ let(:bot) { LLM::Chat.new(provider).lazy }
148
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
149
+
150
+ before do
151
+ bot.respond([
152
+ "Is this PDF document about FreeBSD?",
153
+ "Answer with yes or no. Nothing else.",
154
+ file
155
+ ])
156
+ end
157
+
158
+ it "describes the document" do
159
+ is_expected.to eq("yes")
160
+ ensure
161
+ provider.files.delete(file:)
162
+ end
163
+ end
164
+
165
+ context "when asked to describe the contents of a file",
166
+ vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_3.pdf"} do
167
+ subject { bot.last_message.content.downcase[0..2] }
168
+ let(:bot) { LLM::Chat.new(provider).lazy }
169
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
170
+
171
+ before do
172
+ bot.chat(file)
173
+ bot.chat("Is this PDF document about FreeBSD?")
174
+ bot.chat("Answer with yes or no. Nothing else.")
175
+ end
176
+
177
+ it "describes the document" do
178
+ is_expected.to eq("yes")
179
+ ensure
180
+ provider.files.delete(file:)
181
+ end
182
+ end
183
+
184
+ context "when asked to describe the contents of a file",
185
+ vcr: {cassette_name: "openai/files/describe_freebsd.sysctl_4.pdf"} do
186
+ subject { bot.last_message.content.downcase[0..2] }
187
+ let(:bot) { LLM::Chat.new(provider).lazy }
188
+ let(:file) { provider.files.create(file: LLM::File("spec/fixtures/documents/freebsd.sysctl.pdf")) }
189
+
190
+ before do
191
+ bot.chat([
192
+ "Is this PDF document about FreeBSD?",
193
+ "Answer with yes or no. Nothing else.",
194
+ file
195
+ ])
196
+ end
197
+
198
+ it "describes the document" do
199
+ is_expected.to eq("yes")
200
+ ensure
201
+ provider.files.delete(file:)
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "setup"
4
+
5
+ RSpec.describe "LLM::OpenAI::Images" do
6
+ let(:token) { ENV["LLM_SECRET"] || "TOKEN" }
7
+ let(:provider) { LLM.openai(token) }
8
+
9
+ context "when given a successful create operation (urls)",
10
+ vcr: {cassette_name: "openai/images/successful_create_urls"} 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 array of urls" do
18
+ expect(response.urls).to be_instance_of(Array)
19
+ end
20
+
21
+ it "returns a url" do
22
+ expect(response.urls[0]).to be_instance_of(String)
23
+ end
24
+ end
25
+
26
+ context "when given a successful create operation (base64)",
27
+ vcr: {cassette_name: "openai/images/successful_create_base64"} do
28
+ subject(:response) do
29
+ provider.images.create(
30
+ prompt: "A dog on a rocket to the moon",
31
+ response_format: "b64_json"
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 an array of images" do
40
+ expect(response.images).to be_instance_of(Array)
41
+ end
42
+
43
+ it "returns an encoded string" do
44
+ expect(response.images[0].encoded).to be_instance_of(String)
45
+ end
46
+
47
+ it "returns an binary string" do
48
+ expect(response.images[0].binary).to be_instance_of(String)
49
+ end
50
+ end
51
+
52
+ context "when given a successful variation operation",
53
+ vcr: {cassette_name: "openai/images/successful_variation"} do
54
+ subject(:response) do
55
+ provider.images.create_variation(
56
+ image: LLM::File("spec/fixtures/images/bluebook.png"),
57
+ n: 5
58
+ )
59
+ end
60
+
61
+ it "is successful" do
62
+ expect(response).to be_instance_of(LLM::Response::Image)
63
+ end
64
+
65
+ it "returns data" do
66
+ expect(response.urls.size).to eq(5)
67
+ end
68
+
69
+ it "returns multiple variations" do
70
+ response.urls.each { expect(_1).to be_instance_of(String) }
71
+ end
72
+ end
73
+
74
+ context "when given a successful edit",
75
+ vcr: {cassette_name: "openai/images/successful_edit"} do
76
+ subject(:response) do
77
+ provider.images.edit(
78
+ image: LLM::File("spec/fixtures/images/bluebook.png"),
79
+ prompt: "Add white background"
80
+ )
81
+ end
82
+
83
+ it "is successful" do
84
+ expect(response).to be_instance_of(LLM::Response::Image)
85
+ end
86
+
87
+ it "returns data" do
88
+ expect(response.urls).to be_instance_of(Array)
89
+ end
90
+
91
+ it "returns a url" do
92
+ expect(response.urls[0]).to be_instance_of(String)
93
+ end
94
+ end
95
+ end