openai.rb 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +122 -0
- data/bin/codegen +371 -0
- data/bin/console +16 -0
- data/lib/openai/version.rb +5 -0
- data/lib/openai.rb +381 -0
- data/openai.gemspec +22 -0
- data/spec/data/sample.jsonl +1 -0
- data/spec/data/sample.mp3 +0 -0
- data/spec/spec_helper.rb +55 -0
- data/spec/unit/openai_spec.rb +760 -0
- metadata +99 -0
data/lib/openai.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concord'
|
4
|
+
require 'anima'
|
5
|
+
require 'http'
|
6
|
+
require 'addressable'
|
7
|
+
|
8
|
+
require 'openai/version'
|
9
|
+
|
10
|
+
class OpenAI
|
11
|
+
include Concord.new(:api_key, :http)
|
12
|
+
|
13
|
+
ResponseError = Class.new(StandardError)
|
14
|
+
|
15
|
+
HOST = Addressable::URI.parse('https://api.openai.com/v1')
|
16
|
+
|
17
|
+
def initialize(api_key, http: HTTP)
|
18
|
+
super(api_key, http)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_completion(model:, **kwargs)
|
22
|
+
Response::Completion.from_json(
|
23
|
+
post('/v1/completions', model: model, **kwargs)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_chat_completion(model:, messages:, **kwargs)
|
28
|
+
Response::ChatCompletion.from_json(
|
29
|
+
post('/v1/chat/completions', model: model, messages: messages, **kwargs)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_embedding(model:, input:, **kwargs)
|
34
|
+
Response::Embedding.from_json(
|
35
|
+
post('/v1/embeddings', model: model, input: input, **kwargs)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def list_models
|
40
|
+
Response::ListModel.from_json(get('/v1/models'))
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_model(model_id)
|
44
|
+
Response::Model.from_json(
|
45
|
+
get("/v1/models/#{model_id}")
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_edit(model:, instruction:, **kwargs)
|
50
|
+
Response::Edit.from_json(
|
51
|
+
post('/v1/edits', model: model, instruction: instruction, **kwargs)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_image_generation(prompt:, **kwargs)
|
56
|
+
Response::ImageGeneration.from_json(
|
57
|
+
post('/v1/images/generations', prompt: prompt, **kwargs)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_file(file:, purpose:)
|
62
|
+
absolute_path = Pathname.new(file).expand_path.to_s
|
63
|
+
form_file = HTTP::FormData::File.new(absolute_path)
|
64
|
+
Response::File.from_json(
|
65
|
+
post_form_multipart('/v1/files', file: form_file, purpose: purpose)
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def list_files
|
70
|
+
Response::FileList.from_json(
|
71
|
+
get('/v1/files')
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_file(file_id)
|
76
|
+
Response::File.from_json(
|
77
|
+
delete("/v1/files/#{file_id}")
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_file(file_id)
|
82
|
+
Response::File.from_json(
|
83
|
+
get("/v1/files/#{file_id}")
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_file_content(file_id)
|
88
|
+
get("/v1/files/#{file_id}/content")
|
89
|
+
end
|
90
|
+
|
91
|
+
def list_fine_tunes
|
92
|
+
Response::FineTuneList.from_json(
|
93
|
+
get('/v1/fine-tunes')
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_fine_tune(training_file:, **kwargs)
|
98
|
+
Response::FineTune.from_json(
|
99
|
+
post('/v1/fine-tunes', training_file: training_file, **kwargs)
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_fine_tune(fine_tune_id)
|
104
|
+
Response::FineTune.from_json(
|
105
|
+
get("/v1/fine-tunes/#{fine_tune_id}")
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
def cancel_fine_tune(fine_tune_id)
|
110
|
+
Response::FineTune.from_json(
|
111
|
+
post("/v1/fine-tunes/#{fine_tune_id}/cancel")
|
112
|
+
)
|
113
|
+
end
|
114
|
+
|
115
|
+
def transcribe_audio(file:, model:, **kwargs)
|
116
|
+
absolute_path = Pathname.new(file).expand_path.to_s
|
117
|
+
form_file = HTTP::FormData::File.new(absolute_path)
|
118
|
+
Response::Transcription.from_json(
|
119
|
+
post_form_multipart(
|
120
|
+
'/v1/audio/transcriptions',
|
121
|
+
file: form_file,
|
122
|
+
model: model,
|
123
|
+
**kwargs
|
124
|
+
)
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
def inspect
|
129
|
+
"#<#{self.class}>"
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def get(route)
|
135
|
+
unwrap_response(json_http_client.get(url_for(route)))
|
136
|
+
end
|
137
|
+
|
138
|
+
def delete(route)
|
139
|
+
unwrap_response(json_http_client.delete(url_for(route)))
|
140
|
+
end
|
141
|
+
|
142
|
+
def post(route, **body)
|
143
|
+
unwrap_response(json_http_client.post(url_for(route), json: body))
|
144
|
+
end
|
145
|
+
|
146
|
+
def post_form_multipart(route, **body)
|
147
|
+
unwrap_response(http_client.post(url_for(route), form: body))
|
148
|
+
end
|
149
|
+
|
150
|
+
def url_for(route)
|
151
|
+
HOST.join(route).to_str
|
152
|
+
end
|
153
|
+
|
154
|
+
def unwrap_response(response)
|
155
|
+
unless response.status.success?
|
156
|
+
raise ResponseError, "Unexpected response #{response.status}\nBody:\n#{response.body}"
|
157
|
+
end
|
158
|
+
|
159
|
+
response.body.to_s
|
160
|
+
end
|
161
|
+
|
162
|
+
def json_http_client
|
163
|
+
http_client.headers('Content-Type' => 'application/json')
|
164
|
+
end
|
165
|
+
|
166
|
+
def http_client
|
167
|
+
http.headers('Authorization' => "Bearer #{api_key}")
|
168
|
+
end
|
169
|
+
|
170
|
+
class Response
|
171
|
+
class JSONPayload
|
172
|
+
include Concord.new(:internal_data)
|
173
|
+
|
174
|
+
def self.from_json(raw_json)
|
175
|
+
new(JSON.parse(raw_json, symbolize_names: true))
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.field(name, path: [name], wrapper: nil)
|
179
|
+
given_wrapper = wrapper
|
180
|
+
define_method(name) do
|
181
|
+
field(path, wrapper: given_wrapper)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.optional_field(name, path: name)
|
186
|
+
define_method(name) do
|
187
|
+
optional_field(path)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def original_payload
|
192
|
+
internal_data
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def optional_field(key_path)
|
198
|
+
*head, tail = key_path
|
199
|
+
|
200
|
+
field(head)[tail]
|
201
|
+
end
|
202
|
+
|
203
|
+
def field(key_path, wrapper: nil)
|
204
|
+
value = key_path.reduce(internal_data, :fetch)
|
205
|
+
return value unless wrapper
|
206
|
+
|
207
|
+
if value.is_a?(Array)
|
208
|
+
value.map { |item| wrapper.new(item) }
|
209
|
+
else
|
210
|
+
wrapper.new(value)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class Completion < JSONPayload
|
216
|
+
class Choice < JSONPayload
|
217
|
+
field :text
|
218
|
+
field :index
|
219
|
+
field :logprobs
|
220
|
+
field :finish_reason
|
221
|
+
end
|
222
|
+
|
223
|
+
class Usage < JSONPayload
|
224
|
+
field :prompt_tokens
|
225
|
+
field :completion_tokens
|
226
|
+
field :total_tokens
|
227
|
+
end
|
228
|
+
|
229
|
+
field :id
|
230
|
+
field :object
|
231
|
+
field :created
|
232
|
+
field :model
|
233
|
+
field :choices, wrapper: Choice
|
234
|
+
field :usage, wrapper: Usage
|
235
|
+
end
|
236
|
+
|
237
|
+
class ChatCompletion < JSONPayload
|
238
|
+
class Choice < JSONPayload
|
239
|
+
class Message < JSONPayload
|
240
|
+
field :role
|
241
|
+
field :content
|
242
|
+
end
|
243
|
+
|
244
|
+
field :index
|
245
|
+
field :message, wrapper: Message
|
246
|
+
field :finish_reason
|
247
|
+
end
|
248
|
+
|
249
|
+
class Usage < JSONPayload
|
250
|
+
field :prompt_tokens
|
251
|
+
field :completion_tokens
|
252
|
+
field :total_tokens
|
253
|
+
end
|
254
|
+
|
255
|
+
field :id
|
256
|
+
field :object
|
257
|
+
field :created
|
258
|
+
field :choices, wrapper: Choice
|
259
|
+
field :usage, wrapper: Usage
|
260
|
+
end
|
261
|
+
|
262
|
+
class Embedding < JSONPayload
|
263
|
+
class EmbeddingData < JSONPayload
|
264
|
+
field :object
|
265
|
+
field :embedding
|
266
|
+
field :index
|
267
|
+
end
|
268
|
+
|
269
|
+
class Usage < JSONPayload
|
270
|
+
field :prompt_tokens
|
271
|
+
field :total_tokens
|
272
|
+
end
|
273
|
+
|
274
|
+
field :object
|
275
|
+
field :data, wrapper: EmbeddingData
|
276
|
+
field :model
|
277
|
+
field :usage, wrapper: Usage
|
278
|
+
end
|
279
|
+
|
280
|
+
class Model < JSONPayload
|
281
|
+
field :id
|
282
|
+
field :object
|
283
|
+
field :owned_by
|
284
|
+
field :permission
|
285
|
+
end
|
286
|
+
|
287
|
+
class ListModel < JSONPayload
|
288
|
+
field :data, wrapper: Model
|
289
|
+
end
|
290
|
+
|
291
|
+
class Edit < JSONPayload
|
292
|
+
class Choice < JSONPayload
|
293
|
+
field :text
|
294
|
+
field :index
|
295
|
+
end
|
296
|
+
|
297
|
+
class Usage < JSONPayload
|
298
|
+
field :prompt_tokens
|
299
|
+
field :completion_tokens
|
300
|
+
field :total_tokens
|
301
|
+
end
|
302
|
+
|
303
|
+
field :object
|
304
|
+
field :created
|
305
|
+
field :choices, wrapper: Choice
|
306
|
+
field :usage, wrapper: Usage
|
307
|
+
end
|
308
|
+
|
309
|
+
class ImageGeneration < JSONPayload
|
310
|
+
class Image < JSONPayload
|
311
|
+
field :url
|
312
|
+
end
|
313
|
+
|
314
|
+
field :created
|
315
|
+
field :data, wrapper: Image
|
316
|
+
end
|
317
|
+
|
318
|
+
class File < JSONPayload
|
319
|
+
field :id
|
320
|
+
field :object
|
321
|
+
field :bytes
|
322
|
+
field :created_at
|
323
|
+
field :filename
|
324
|
+
field :purpose
|
325
|
+
optional_field :deleted?, path: :deleted
|
326
|
+
end
|
327
|
+
|
328
|
+
class FileList < JSONPayload
|
329
|
+
field :data, wrapper: File
|
330
|
+
field :object
|
331
|
+
end
|
332
|
+
|
333
|
+
class FineTune < JSONPayload
|
334
|
+
class Event < JSONPayload
|
335
|
+
field :object
|
336
|
+
field :created_at
|
337
|
+
field :level
|
338
|
+
field :message
|
339
|
+
end
|
340
|
+
|
341
|
+
class Hyperparams < JSONPayload
|
342
|
+
field :batch_size
|
343
|
+
field :learning_rate_multiplier
|
344
|
+
field :n_epochs
|
345
|
+
field :prompt_loss_weight
|
346
|
+
end
|
347
|
+
|
348
|
+
class File < JSONPayload
|
349
|
+
field :id
|
350
|
+
field :object
|
351
|
+
field :bytes
|
352
|
+
field :created_at
|
353
|
+
field :filename
|
354
|
+
field :purpose
|
355
|
+
end
|
356
|
+
|
357
|
+
field :id
|
358
|
+
field :object
|
359
|
+
field :model
|
360
|
+
field :created_at
|
361
|
+
field :events, wrapper: Event
|
362
|
+
field :fine_tuned_model
|
363
|
+
field :hyperparams, wrapper: Hyperparams
|
364
|
+
field :organization_id
|
365
|
+
field :result_files, wrapper: File
|
366
|
+
field :status
|
367
|
+
field :validation_files, wrapper: File
|
368
|
+
field :training_files, wrapper: File
|
369
|
+
field :updated_at
|
370
|
+
end
|
371
|
+
|
372
|
+
class FineTuneList < JSONPayload
|
373
|
+
field :object
|
374
|
+
field :data, wrapper: FineTune
|
375
|
+
end
|
376
|
+
|
377
|
+
class Transcription < JSONPayload
|
378
|
+
field :text
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
data/openai.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('lib/openai/version', __dir__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'openai.rb'
|
7
|
+
spec.version = OpenAI::VERSION
|
8
|
+
spec.authors = %w[John Backus]
|
9
|
+
spec.email = %w[johncbackus@gmail.com]
|
10
|
+
|
11
|
+
spec.summary = 'OpenAI Ruby Wrapper'
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = 'https://github.com/backus/openai-ruby'
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split("\n")
|
16
|
+
spec.require_paths = %w[lib]
|
17
|
+
spec.executables = []
|
18
|
+
|
19
|
+
spec.add_dependency 'anima', '~> 0.3'
|
20
|
+
spec.add_dependency 'concord', '~> 0.1'
|
21
|
+
spec.add_dependency 'http', '~> 5.1'
|
22
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
{"prompt": "Say hello", "completion": "hello"}
|
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openai'
|
4
|
+
require 'pry'
|
5
|
+
require 'pry-byebug'
|
6
|
+
|
7
|
+
module OpenAISpec
|
8
|
+
ROOT = Pathname.new(__dir__).parent
|
9
|
+
SPEC_ROOT = ROOT.join('spec')
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec.configure do |config|
|
13
|
+
# Enable focused tests and run all tests if nothing is focused
|
14
|
+
config.filter_run_when_matching(:focus)
|
15
|
+
|
16
|
+
# Forbid RSpec from monkey patching any of our objects
|
17
|
+
config.disable_monkey_patching!
|
18
|
+
|
19
|
+
# We should address configuration warnings when we upgrade
|
20
|
+
config.raise_errors_for_deprecations!
|
21
|
+
|
22
|
+
# RSpec gives helpful warnings when you are doing something wrong.
|
23
|
+
# We should take their advice!
|
24
|
+
config.raise_on_warning = true
|
25
|
+
|
26
|
+
config.mock_with(:rspec) do |mocks|
|
27
|
+
# Verifies stubbed methods on real objects.
|
28
|
+
# @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/partial-doubles
|
29
|
+
mocks.verify_partial_doubles = true
|
30
|
+
end
|
31
|
+
|
32
|
+
config.expect_with(:rspec) do |expectations|
|
33
|
+
# Set a higher max length for RSpec's test failure output, so it doesn't as aggressively
|
34
|
+
# truncate test failure explanations, obscuring the test failure reason in the process
|
35
|
+
# @note The default output length is 200 characters
|
36
|
+
# @see https://www.rubydoc.info/github/rspec/rspec-expectations/RSpec%2FExpectations%2FConfiguration:max_formatted_output_length=
|
37
|
+
expectations.max_formatted_output_length = 600
|
38
|
+
end
|
39
|
+
|
40
|
+
# Write rspec results to a temporary file so that we can use `rspec --only-failures`
|
41
|
+
config.example_status_persistence_file_path =
|
42
|
+
OpenAISpec::ROOT.join('tmp', 'rspec.txt').to_s
|
43
|
+
|
44
|
+
# Always aggregate failures when there are multiple expectations. This makes debugging tests with
|
45
|
+
# multiple expectations much easier because we can see the failures of each expectation together.
|
46
|
+
config.define_derived_metadata do |metadata|
|
47
|
+
metadata[:aggregate_failures] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
# Define metadata for all tests which live under spec/unit
|
51
|
+
config.define_derived_metadata(file_path: %r{\bspec/unit/}) do |metadata|
|
52
|
+
# Set the type of these tests as 'unit'
|
53
|
+
metadata[:type] = :unit
|
54
|
+
end
|
55
|
+
end
|