openai.rb 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#fine_tunes' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.fine_tunes }
7
+ let(:response_body) do
8
+ {
9
+ "object": 'list',
10
+ "data": [
11
+ {
12
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
13
+ "object": 'fine-tune',
14
+ "model": 'curie',
15
+ "created_at": 1_614_807_352,
16
+ "fine_tuned_model": nil,
17
+ "hyperparams": {},
18
+ "organization_id": 'org-...',
19
+ "result_files": [],
20
+ "status": 'pending',
21
+ "validation_files": [],
22
+ "training_files": [{}],
23
+ "updated_at": 1_614_807_352
24
+ },
25
+ {},
26
+ {}
27
+ ]
28
+ }
29
+ end
30
+
31
+ context 'when listing fine-tunes' do
32
+ it 'can get a list of fine-tunes' do
33
+ fine_tunes = resource.list
34
+
35
+ expect(http)
36
+ .to have_received(:get)
37
+ .with('https://api.openai.com/v1/fine-tunes')
38
+
39
+ expect(fine_tunes.object).to eql('list')
40
+ expect(fine_tunes.data.size).to eql(3)
41
+ expect(fine_tunes.data.first.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
42
+ expect(fine_tunes.data.first.object).to eql('fine-tune')
43
+ expect(fine_tunes.data.first.model).to eql('curie')
44
+ expect(fine_tunes.data.first.created_at).to eql(1_614_807_352)
45
+ expect(fine_tunes.data.first.fine_tuned_model).to be_nil
46
+ expect(fine_tunes.data.first.hyperparams).to eql(
47
+ described_class::Response::FineTune::Hyperparams.new({})
48
+ )
49
+ expect(fine_tunes.data.first.organization_id).to eql('org-...')
50
+ expect(fine_tunes.data.first.result_files).to eql([])
51
+ expect(fine_tunes.data.first.status).to eql('pending')
52
+ expect(fine_tunes.data.first.validation_files).to eql([])
53
+ expect(fine_tunes.data.first.training_files).to eql(
54
+ [
55
+ described_class::Response::FineTune::File.new({})
56
+ ]
57
+ )
58
+ expect(fine_tunes.data.first.updated_at).to eql(1_614_807_352)
59
+ end
60
+ end
61
+
62
+ context 'when creating a fine-tune' do
63
+ let(:response_body) do
64
+ {
65
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
66
+ "object": 'fine-tune',
67
+ "model": 'curie',
68
+ "created_at": 1_614_807_352,
69
+ "events": [
70
+ {
71
+ "object": 'fine-tune-event',
72
+ "created_at": 1_614_807_352,
73
+ "level": 'info',
74
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
75
+ }
76
+ ],
77
+ "fine_tuned_model": nil,
78
+ "hyperparams": {
79
+ "batch_size": 4,
80
+ "learning_rate_multiplier": 0.1,
81
+ "n_epochs": 4,
82
+ "prompt_loss_weight": 0.1
83
+ },
84
+ "organization_id": 'org-...',
85
+ "result_files": [],
86
+ "status": 'pending',
87
+ "validation_files": [],
88
+ "training_files": [
89
+ {
90
+ "id": 'file-XGinujblHPwGLSztz8cPS8XY',
91
+ "object": 'file',
92
+ "bytes": 1_547_276,
93
+ "created_at": 1_610_062_281,
94
+ "filename": 'my-data-train.jsonl',
95
+ "purpose": 'fine-tune-train'
96
+ }
97
+ ],
98
+ "updated_at": 1_614_807_352
99
+ }
100
+ end
101
+
102
+ it 'can create a fine-tune' do
103
+ fine_tune = resource.create(training_file: 'my-data-train.jsonl', model: 'curie')
104
+
105
+ expect(http)
106
+ .to have_received(:post)
107
+ .with('https://api.openai.com/v1/fine-tunes', hash_including(:json))
108
+
109
+ expect(fine_tune.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
110
+ expect(fine_tune.model).to eql('curie')
111
+ expect(fine_tune.created_at).to eql(1_614_807_352)
112
+ expect(fine_tune.events.first.object).to eql('fine-tune-event')
113
+ expect(fine_tune.events.first.created_at).to eql(1_614_807_352)
114
+ expect(fine_tune.events.first.level).to eql('info')
115
+ expect(fine_tune.events.first.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
116
+ expect(fine_tune.fine_tuned_model).to be_nil
117
+ expect(fine_tune.hyperparams.batch_size).to eql(4)
118
+ expect(fine_tune.hyperparams.learning_rate_multiplier).to eql(0.1)
119
+ expect(fine_tune.hyperparams.n_epochs).to eql(4)
120
+ expect(fine_tune.hyperparams.prompt_loss_weight).to eql(0.1)
121
+ expect(fine_tune.organization_id).to eql('org-...')
122
+ expect(fine_tune.result_files).to be_empty
123
+ expect(fine_tune.status).to eql('pending')
124
+ expect(fine_tune.validation_files).to be_empty
125
+ expect(fine_tune.training_files.first.id).to eql('file-XGinujblHPwGLSztz8cPS8XY')
126
+ expect(fine_tune.training_files.first.object).to eql('file')
127
+ expect(fine_tune.training_files.first.bytes).to eql(1_547_276)
128
+ expect(fine_tune.training_files.first.created_at).to eql(1_610_062_281)
129
+ expect(fine_tune.training_files.first.filename).to eql('my-data-train.jsonl')
130
+ expect(fine_tune.training_files.first.purpose).to eql('fine-tune-train')
131
+ expect(fine_tune.updated_at).to eql(1_614_807_352)
132
+ end
133
+ end
134
+
135
+ context 'when fetching a fine tune' do
136
+ let(:response_body) do
137
+ {
138
+ "id": 'ft-AF1WoRqd3aJAHsqc9NY7iL8F',
139
+ "object": 'fine-tune',
140
+ "model": 'curie',
141
+ "created_at": 1_614_807_352,
142
+ "events": [
143
+ {
144
+ "object": 'fine-tune-event',
145
+ "created_at": 1_614_807_352,
146
+ "level": 'info',
147
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
148
+ },
149
+ {
150
+ "object": 'fine-tune-event',
151
+ "created_at": 1_614_807_356,
152
+ "level": 'info',
153
+ "message": 'Job started.'
154
+ },
155
+ {
156
+ "object": 'fine-tune-event',
157
+ "created_at": 1_614_807_861,
158
+ "level": 'info',
159
+ "message": 'Uploaded snapshot: curie:ft-acmeco-2021-03-03-21-44-20.'
160
+ },
161
+ {
162
+ "object": 'fine-tune-event',
163
+ "created_at": 1_614_807_864,
164
+ "level": 'info',
165
+ "message": 'Uploaded result files: file-QQm6ZpqdNwAaVC3aSz5sWwLT.'
166
+ },
167
+ {
168
+ "object": 'fine-tune-event',
169
+ "created_at": 1_614_807_864,
170
+ "level": 'info',
171
+ "message": 'Job succeeded.'
172
+ }
173
+ ],
174
+ "fine_tuned_model": 'curie:ft-acmeco-2021-03-03-21-44-20',
175
+ "hyperparams": {
176
+ "batch_size": 4,
177
+ "learning_rate_multiplier": 0.1,
178
+ "n_epochs": 4,
179
+ "prompt_loss_weight": 0.1
180
+ },
181
+ "organization_id": 'org-...',
182
+ "result_files": [
183
+ {
184
+ "id": 'file-QQm6ZpqdNwAaVC3aSz5sWwLT',
185
+ "object": 'file',
186
+ "bytes": 81_509,
187
+ "created_at": 1_614_807_863,
188
+ "filename": 'compiled_results.csv',
189
+ "purpose": 'fine-tune-results'
190
+ }
191
+ ],
192
+ "status": 'succeeded',
193
+ "validation_files": [],
194
+ "training_files": [
195
+ {
196
+ "id": 'file-XGinujblHPwGLSztz8cPS8XY',
197
+ "object": 'file',
198
+ "bytes": 1_547_276,
199
+ "created_at": 1_610_062_281,
200
+ "filename": 'my-data-train.jsonl',
201
+ "purpose": 'fine-tune-train'
202
+ }
203
+ ],
204
+ "updated_at": 1_614_807_865
205
+ }
206
+ end
207
+
208
+ it 'can get a fine-tune' do
209
+ fine_tune = resource.fetch('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
210
+
211
+ expect(http)
212
+ .to have_received(:get)
213
+ .with('https://api.openai.com/v1/fine-tunes/ft-AF1WoRqd3aJAHsqc9NY7iL8F')
214
+
215
+ expect(fine_tune.id).to eql('ft-AF1WoRqd3aJAHsqc9NY7iL8F')
216
+ expect(fine_tune.model).to eql('curie')
217
+ expect(fine_tune.created_at).to eql(1_614_807_352)
218
+ expect(fine_tune.events.first.object).to eql('fine-tune-event')
219
+ expect(fine_tune.events.first.created_at).to eql(1_614_807_352)
220
+ expect(fine_tune.events.first.level).to eql('info')
221
+ expect(fine_tune.events.first.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
222
+ expect(fine_tune.fine_tuned_model).to eql('curie:ft-acmeco-2021-03-03-21-44-20')
223
+ expect(fine_tune.hyperparams.batch_size).to eql(4)
224
+ expect(fine_tune.hyperparams.learning_rate_multiplier).to eql(0.1)
225
+ expect(fine_tune.hyperparams.n_epochs).to eql(4)
226
+ expect(fine_tune.hyperparams.prompt_loss_weight).to eql(0.1)
227
+ expect(fine_tune.organization_id).to eql('org-...')
228
+ expect(fine_tune.result_files.first.id).to eql('file-QQm6ZpqdNwAaVC3aSz5sWwLT')
229
+ expect(fine_tune.result_files.first.object).to eql('file')
230
+ expect(fine_tune.result_files.first.bytes).to eql(81_509)
231
+ expect(fine_tune.result_files.first.created_at).to eql(1_614_807_863)
232
+ expect(fine_tune.result_files.first.filename).to eql('compiled_results.csv')
233
+ expect(fine_tune.result_files.first.purpose).to eql('fine-tune-results')
234
+ expect(fine_tune.status).to eql('succeeded')
235
+ expect(fine_tune.validation_files).to be_empty
236
+ expect(fine_tune.training_files.first.id).to eql('file-XGinujblHPwGLSztz8cPS8XY')
237
+ expect(fine_tune.training_files.first.object).to eql('file')
238
+ expect(fine_tune.training_files.first.bytes).to eql(1_547_276)
239
+ expect(fine_tune.training_files.first.created_at).to eql(1_610_062_281)
240
+ expect(fine_tune.training_files.first.filename).to eql('my-data-train.jsonl')
241
+ expect(fine_tune.training_files.first.purpose).to eql('fine-tune-train')
242
+ expect(fine_tune.updated_at).to eql(1_614_807_865)
243
+ end
244
+ end
245
+
246
+ context 'when canceling a fine-tune' do
247
+ let(:response_body) do
248
+ {
249
+ "id": 'ft-xhrpBbvVUzYGo8oUO1FY4nI7',
250
+ "status": 'cancelled'
251
+ }
252
+ end
253
+
254
+ it 'can cancel a fine-tune' do
255
+ fine_tune = resource.cancel('ft-xhrpBbvVUzYGo8oUO1FY4nI7')
256
+
257
+ expect(http)
258
+ .to have_received(:post)
259
+ .with('https://api.openai.com/v1/fine-tunes/ft-xhrpBbvVUzYGo8oUO1FY4nI7/cancel', json: {})
260
+
261
+ expect(fine_tune.id).to eql('ft-xhrpBbvVUzYGo8oUO1FY4nI7')
262
+ expect(fine_tune.status).to eql('cancelled')
263
+ end
264
+ end
265
+
266
+ context 'when listing fine-tune events' do
267
+ let(:response_body) do
268
+ {
269
+ "object": 'list',
270
+ "data": [
271
+ {
272
+ "object": 'fine-tune-event',
273
+ "created_at": 1_614_807_352,
274
+ "level": 'info',
275
+ "message": 'Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.'
276
+ },
277
+ {
278
+ "object": 'fine-tune-event',
279
+ "created_at": 1_614_807_356,
280
+ "level": 'info',
281
+ "message": 'Job started.'
282
+ },
283
+ {
284
+ "object": 'fine-tune-event',
285
+ "created_at": 1_614_807_861,
286
+ "level": 'info',
287
+ "message": 'Uploaded snapshot: curie:ft-acmeco-2021-03-03-21-44-20.'
288
+ },
289
+ {
290
+ "object": 'fine-tune-event',
291
+ "created_at": 1_614_807_864,
292
+ "level": 'info',
293
+ "message": 'Uploaded result files: file-QQm6ZpqdNwAaVC3aSz5sWwLT.'
294
+ },
295
+ {
296
+ "object": 'fine-tune-event',
297
+ "created_at": 1_614_807_864,
298
+ "level": 'info',
299
+ "message": 'Job succeeded.'
300
+ }
301
+ ]
302
+ }
303
+ end
304
+
305
+ it 'can list fine-tune events' do
306
+ events = resource.list_events('fine-tune-id')
307
+
308
+ expect(http)
309
+ .to have_received(:get)
310
+ .with('https://api.openai.com/v1/fine-tunes/fine-tune-id/events')
311
+
312
+ expect(events.object).to eql('list')
313
+ expect(events.data.size).to eql(5)
314
+
315
+ event = events.data.first
316
+ expect(event.object).to eql('fine-tune-event')
317
+ expect(event.created_at).to eql(1_614_807_352)
318
+ expect(event.level).to eql('info')
319
+ expect(event.message).to eql('Job enqueued. Waiting for jobs ahead to complete. Queue number: 0.')
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#images' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.images }
7
+
8
+ let(:sample_image) { OpenAISpec::SPEC_ROOT.join('data/sample_image.png') }
9
+
10
+ context 'when creating an image' do
11
+ let(:response_body) do
12
+ {
13
+ created: Time.now.to_i,
14
+ data: [
15
+ { url: 'https://example.com/image1.png' },
16
+ { url: 'https://example.com/image2.png' }
17
+ ]
18
+ }
19
+ end
20
+
21
+ it 'can create an image generation' do
22
+ image_generation = resource.create(prompt: 'a bird in the forest', size: 512)
23
+
24
+ expect(http)
25
+ .to have_received(:post)
26
+ .with(
27
+ 'https://api.openai.com/v1/images/generations',
28
+ hash_including(
29
+ json: hash_including(prompt: 'a bird in the forest', size: 512)
30
+ )
31
+ )
32
+
33
+ expect(image_generation.created).to be_within(1).of(Time.now.to_i)
34
+ expect(image_generation.data.map(&:url)).to contain_exactly('https://example.com/image1.png', 'https://example.com/image2.png')
35
+ end
36
+ end
37
+
38
+ context 'when editing an image' do
39
+ let(:sample_mask) { OpenAISpec::SPEC_ROOT.join('data/sample_image_mask.png') }
40
+
41
+ let(:response_body) do
42
+ {
43
+ "created": 1_589_478_378,
44
+ "data": [
45
+ {
46
+ "url": 'https://...'
47
+ },
48
+ {
49
+ "url": 'https://...'
50
+ }
51
+ ]
52
+ }
53
+ end
54
+
55
+ it 'can edit an image' do
56
+ image_edit = resource.edit(
57
+ image: sample_image,
58
+ mask: sample_mask,
59
+ prompt: 'Draw a red hat on the person in the image',
60
+ n: 1,
61
+ size: '512x512',
62
+ response_format: 'url',
63
+ user: 'user-123'
64
+ )
65
+
66
+ expect(http)
67
+ .to have_received(:post)
68
+ .with(
69
+ 'https://api.openai.com/v1/images/edits',
70
+ hash_including(
71
+ form: hash_including(
72
+ {
73
+ image: instance_of(HTTP::FormData::File),
74
+ mask: instance_of(HTTP::FormData::File),
75
+ prompt: 'Draw a red hat on the person in the image',
76
+ n: 1,
77
+ size: '512x512',
78
+ response_format: 'url',
79
+ user: 'user-123'
80
+ }
81
+ )
82
+ )
83
+ )
84
+
85
+ expect(image_edit.created).to eql(1_589_478_378)
86
+ expect(image_edit.data.first.url).to eql('https://...')
87
+ end
88
+ end
89
+
90
+ context 'when creating image variations' do
91
+ let(:response_body) do
92
+ {
93
+ "created": 1_589_478_378,
94
+ "data": [
95
+ {
96
+ "url": 'https://...'
97
+ },
98
+ {
99
+ "url": 'https://...'
100
+ }
101
+ ]
102
+ }
103
+ end
104
+
105
+ it 'can create image variations' do
106
+ image_variations = resource.create_variation(
107
+ image: sample_image,
108
+ n: 2,
109
+ size: '512x512',
110
+ response_format: 'url',
111
+ user: 'user123'
112
+ )
113
+
114
+ expect(http)
115
+ .to have_received(:post)
116
+ .with(
117
+ 'https://api.openai.com/v1/images/variations',
118
+ hash_including(
119
+ form: hash_including(
120
+ {
121
+ image: instance_of(HTTP::FormData::File),
122
+ n: 2,
123
+ size: '512x512',
124
+ response_format: 'url',
125
+ user: 'user123'
126
+ }
127
+ )
128
+ )
129
+ )
130
+
131
+ expect(image_variations.created).to eql(1_589_478_378)
132
+ expect(image_variations.data.size).to eql(2)
133
+ expect(image_variations.data.first.url).to eql('https://...')
134
+ expect(image_variations.data.last.url).to eql('https://...')
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe OpenAI::API, '#models' do
4
+ include_context 'an API Resource'
5
+
6
+ let(:resource) { api.models }
7
+
8
+ context 'when listing models' do
9
+ let(:response_body) do
10
+ {
11
+ data: [
12
+ {
13
+ id: 'model-id-0',
14
+ object: 'model',
15
+ owned_by: 'organization-owner',
16
+ permission: [1, 2, 3]
17
+ },
18
+ {
19
+ id: 'model-id-1',
20
+ object: 'model',
21
+ owned_by: 'organization-owner',
22
+ permission: [4, 5, 6]
23
+ },
24
+ {
25
+ id: 'model-id-2',
26
+ object: 'model',
27
+ owned_by: 'openai',
28
+ permission: [7, 8, 9]
29
+ }
30
+ ],
31
+ object: 'list'
32
+ }
33
+ end
34
+
35
+ it 'can list all models' do
36
+ models = resource.list
37
+
38
+ expect(http)
39
+ .to have_received(:get)
40
+ .with('https://api.openai.com/v1/models')
41
+
42
+ expect(models.data.length).to eql(3)
43
+
44
+ expect(models.data[0].id).to eql('model-id-0')
45
+ expect(models.data[0].object).to eql('model')
46
+ expect(models.data[0].owned_by).to eql('organization-owner')
47
+ expect(models.data[0].permission).to eql([1, 2, 3])
48
+
49
+ expect(models.data[1].id).to eql('model-id-1')
50
+ expect(models.data[1].object).to eql('model')
51
+ expect(models.data[1].owned_by).to eql('organization-owner')
52
+ expect(models.data[1].permission).to eql([4, 5, 6])
53
+
54
+ expect(models.data[2].id).to eql('model-id-2')
55
+ expect(models.data[2].object).to eql('model')
56
+ expect(models.data[2].owned_by).to eql('openai')
57
+ expect(models.data[2].permission).to eql([7, 8, 9])
58
+ end
59
+ end
60
+
61
+ context 'when retrieving a model' do
62
+ let(:response_body) do
63
+ {
64
+ "id": 'text-davinci-002',
65
+ "object": 'model',
66
+ "owned_by": 'openai',
67
+ "permission": %w[
68
+ query
69
+ completions
70
+ models:read
71
+ models:write
72
+ engine:read
73
+ engine:write
74
+ ]
75
+ }
76
+ end
77
+
78
+ it 'can retrieve a model' do
79
+ model = resource.fetch('text-davinci-002')
80
+
81
+ expect(http)
82
+ .to have_received(:get)
83
+ .with('https://api.openai.com/v1/models/text-davinci-002')
84
+
85
+ expect(model.id).to eql('text-davinci-002')
86
+ expect(model.object).to eql('model')
87
+ expect(model.owned_by).to eql('openai')
88
+ expect(model.permission).to eql(%w[
89
+ query
90
+ completions
91
+ models:read
92
+ models:write
93
+ engine:read
94
+ engine:write
95
+ ])
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ RSpec.describe OpenAI::API, '#moderations' do
2
+ include_context 'an API Resource'
3
+
4
+ let(:resource) { api.moderations }
5
+
6
+ let(:response_body) do
7
+ {
8
+ "id": 'modr-5MWoLO',
9
+ "model": 'text-moderation-001',
10
+ "results": [
11
+ {
12
+ "categories": {
13
+ "hate": false,
14
+ "hate/threatening": true,
15
+ "self-harm": false,
16
+ "sexual": false,
17
+ "sexual/minors": false,
18
+ "violence": true,
19
+ "violence/graphic": false
20
+ },
21
+ "category_scores": {
22
+ "hate": 0.22714105248451233,
23
+ "hate/threatening": 0.4132447838783264,
24
+ "self-harm": 0.005232391878962517,
25
+ "sexual": 0.01407341007143259,
26
+ "sexual/minors": 0.0038522258400917053,
27
+ "violence": 0.9223177433013916,
28
+ "violence/graphic": 0.036865197122097015
29
+ },
30
+ "flagged": true
31
+ }
32
+ ]
33
+ }
34
+ end
35
+
36
+ it 'can create a moderation' do
37
+ moderation = resource.create(input: 'This is a test', model: 'text-moderation-001')
38
+
39
+ expect(http)
40
+ .to have_received(:post)
41
+ .with('https://api.openai.com/v1/moderations', hash_including(:json))
42
+
43
+ expect(moderation.id).to eql('modr-5MWoLO')
44
+ expect(moderation.model).to eql('text-moderation-001')
45
+ expect(moderation.results.first.categories.hate).to be_falsey
46
+ expect(moderation.results.first.categories.hate_threatening).to be_truthy
47
+ expect(moderation.results.first.categories.self_harm).to be_falsey
48
+ expect(moderation.results.first.categories.sexual).to be_falsey
49
+ expect(moderation.results.first.categories.sexual_minors).to be_falsey
50
+ expect(moderation.results.first.categories.violence).to be_truthy
51
+ expect(moderation.results.first.categories.violence_graphic).to be_falsey
52
+ expect(moderation.results.first.category_scores.hate).to eql(0.22714105248451233)
53
+ expect(moderation.results.first.category_scores.hate_threatening).to eql(0.4132447838783264)
54
+ expect(moderation.results.first.category_scores.self_harm).to eql(0.005232391878962517)
55
+ expect(moderation.results.first.category_scores.sexual).to eql(0.01407341007143259)
56
+ expect(moderation.results.first.category_scores.sexual_minors).to eql(0.0038522258400917053)
57
+ expect(moderation.results.first.category_scores.violence).to eql(0.9223177433013916)
58
+ expect(moderation.results.first.category_scores.violence_graphic).to eql(0.036865197122097015)
59
+ expect(moderation.results.first.flagged).to be_truthy
60
+ end
61
+ end