openai.rb 0.0.0 → 0.0.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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +4 -4
- data/Gemfile.lock +17 -14
- data/README.md +401 -0
- data/bin/codegen +64 -55
- data/bin/console +7 -1
- data/lib/openai/api/cache.rb +137 -0
- data/lib/openai/api/client.rb +86 -0
- data/lib/openai/api/resource.rb +235 -0
- data/lib/openai/api/response.rb +352 -0
- data/lib/openai/api.rb +61 -0
- data/lib/openai/chat.rb +75 -0
- data/lib/openai/tokenizer.rb +50 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +29 -358
- data/openai.gemspec +7 -3
- data/spec/data/sample_french.mp3 +0 -0
- data/spec/data/sample_image.png +0 -0
- data/spec/data/sample_image_mask.png +0 -0
- data/spec/shared/api_resource_context.rb +22 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/unit/openai/api/audio_spec.rb +78 -0
- data/spec/unit/openai/api/cache_spec.rb +115 -0
- data/spec/unit/openai/api/chat_completions_spec.rb +116 -0
- data/spec/unit/openai/api/completions_spec.rb +119 -0
- data/spec/unit/openai/api/edits_spec.rb +40 -0
- data/spec/unit/openai/api/embeddings_spec.rb +45 -0
- data/spec/unit/openai/api/files_spec.rb +163 -0
- data/spec/unit/openai/api/fine_tunes_spec.rb +322 -0
- data/spec/unit/openai/api/images_spec.rb +137 -0
- data/spec/unit/openai/api/models_spec.rb +98 -0
- data/spec/unit/openai/api/moderations_spec.rb +61 -0
- data/spec/unit/openai/api/response_spec.rb +203 -0
- data/spec/unit/openai/tokenizer_spec.rb +45 -0
- data/spec/unit/openai_spec.rb +47 -736
- metadata +83 -2
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe OpenAI::API::Response do
|
4
|
+
before do
|
5
|
+
user_class = Class.new(described_class) do
|
6
|
+
field :username, path: %i[handle]
|
7
|
+
end
|
8
|
+
|
9
|
+
stub_const('OpenAISpec::SampleResponse::User', user_class)
|
10
|
+
|
11
|
+
comment_class = Class.new(described_class) do
|
12
|
+
field :body
|
13
|
+
field :user, wrapper: OpenAISpec::SampleResponse::User
|
14
|
+
end
|
15
|
+
|
16
|
+
stub_const('OpenAISpec::SampleResponse::Comment', comment_class)
|
17
|
+
|
18
|
+
post_class = Class.new(described_class) do
|
19
|
+
field :created_at, path: %i[meta birth created]
|
20
|
+
field :text
|
21
|
+
field :comments, wrapper: OpenAISpec::SampleResponse::Comment
|
22
|
+
field :author, wrapper: OpenAISpec::SampleResponse::User
|
23
|
+
optional_field :co_author, wrapper: OpenAISpec::SampleResponse::User
|
24
|
+
optional_field :subtitle
|
25
|
+
|
26
|
+
# For demonstrating that we can use the instance method without specifying
|
27
|
+
# a wrapper class
|
28
|
+
define_method(:other_author) do
|
29
|
+
optional_field([:co_author])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
stub_const('OpenAISpec::SampleResponse::Post', post_class)
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:sample_response) do
|
37
|
+
OpenAISpec::SampleResponse::Post.new(sample_response_payload)
|
38
|
+
end
|
39
|
+
|
40
|
+
let(:sample_response_payload) do
|
41
|
+
{
|
42
|
+
meta: {
|
43
|
+
birth: {
|
44
|
+
created: Time.new(2023).to_i
|
45
|
+
}
|
46
|
+
},
|
47
|
+
text: 'This is a post',
|
48
|
+
comments: [
|
49
|
+
{
|
50
|
+
body: 'This is a comment',
|
51
|
+
user: {
|
52
|
+
handle: 'alice'
|
53
|
+
}
|
54
|
+
},
|
55
|
+
{
|
56
|
+
body: 'This is a spicy comment',
|
57
|
+
user: {
|
58
|
+
handle: 'bob'
|
59
|
+
}
|
60
|
+
}
|
61
|
+
],
|
62
|
+
author: {
|
63
|
+
handle: 'carl'
|
64
|
+
}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when inspecting the response' do
|
69
|
+
# Define a smaller response payload so this is less annoying to test
|
70
|
+
let(:sample_response_payload) do
|
71
|
+
{
|
72
|
+
meta: { birth: { created: 1234 } },
|
73
|
+
text: 'This is a post',
|
74
|
+
comments: [],
|
75
|
+
author: { handle: 'carl' }
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
before do
|
80
|
+
# Mark a field as private so we can prove that the #inspect method
|
81
|
+
# should use __send__ in case a response class chooses to make a
|
82
|
+
# field private.
|
83
|
+
OpenAISpec::SampleResponse::Post.class_eval do
|
84
|
+
private(:created_at)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'defines a nice clean inspect method' do
|
89
|
+
expect(sample_response.inspect).to eql(
|
90
|
+
'#<OpenAISpec::SampleResponse::Post ' \
|
91
|
+
'created_at=1234 ' \
|
92
|
+
'text="This is a post" ' \
|
93
|
+
'comments=[] ' \
|
94
|
+
'author=#<OpenAISpec::SampleResponse::User username="carl"> ' \
|
95
|
+
'co_author=nil ' \
|
96
|
+
'subtitle=nil>'
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'tracks the fields on a class for the sake of the #inspect method' do
|
101
|
+
expect(OpenAISpec::SampleResponse::Comment.__send__(:field_registry))
|
102
|
+
.to eql(%i[body user])
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'can parse a JSON response' do
|
107
|
+
expect(
|
108
|
+
OpenAISpec::SampleResponse::Post.from_json(
|
109
|
+
JSON.dump(sample_response_payload)
|
110
|
+
)
|
111
|
+
).to eql(sample_response)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'exposes the original payload' do
|
115
|
+
expect(sample_response.original_payload).to eql(sample_response_payload)
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'deep freezes the original payload' do
|
119
|
+
original = sample_response.original_payload
|
120
|
+
expect(original).to be_frozen
|
121
|
+
expect(original[:comments]).to be_frozen
|
122
|
+
expect(original[:comments].first).to be_frozen
|
123
|
+
expect(original[:comments].first[:user]).to be_frozen
|
124
|
+
end
|
125
|
+
|
126
|
+
describe '.field' do
|
127
|
+
it 'exposes the field' do
|
128
|
+
expect(sample_response.text).to eql('This is a post')
|
129
|
+
expect(sample_response.created_at).to eql(1_672_549_200)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'can expose fields under a different name than the key path' do
|
133
|
+
expect(sample_response.author.username).to eql('carl')
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'wraps the field if a wrapper is provided' do
|
137
|
+
expect(sample_response.author).to eql(
|
138
|
+
OpenAISpec::SampleResponse::User.new(handle: 'carl')
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'wraps each element in a the wrapper if the value is an array' do
|
143
|
+
expect(sample_response.comments).to all(
|
144
|
+
be_an_instance_of(OpenAISpec::SampleResponse::Comment)
|
145
|
+
)
|
146
|
+
expect(sample_response.comments[0].user).to eql(
|
147
|
+
OpenAISpec::SampleResponse::User.new(handle: 'alice')
|
148
|
+
)
|
149
|
+
expect(sample_response.comments[1].user).to eql(
|
150
|
+
OpenAISpec::SampleResponse::User.new(handle: 'bob')
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'when a required field is not present' do
|
155
|
+
let(:sample_response_payload) do
|
156
|
+
{ meta: { error: 'you did something wrong bro' } }
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'raises an error when the field is accessed' do
|
160
|
+
expect { sample_response.text }.to raise_error(
|
161
|
+
described_class::MissingFieldError, <<~ERROR
|
162
|
+
Missing field :text in response payload!
|
163
|
+
Was attempting to access value at path `[:text]`.
|
164
|
+
Payload: {
|
165
|
+
"meta": {
|
166
|
+
"error": "you did something wrong bro"
|
167
|
+
}
|
168
|
+
}
|
169
|
+
ERROR
|
170
|
+
)
|
171
|
+
|
172
|
+
expect { sample_response.created_at }.to raise_error(
|
173
|
+
described_class::MissingFieldError, <<~ERROR
|
174
|
+
Missing field :birth in response payload!
|
175
|
+
Was attempting to access value at path `[:meta, :birth, :created]`.
|
176
|
+
Payload: {
|
177
|
+
"meta": {
|
178
|
+
"error": "you did something wrong bro"
|
179
|
+
}
|
180
|
+
}
|
181
|
+
ERROR
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe '.optional_field' do
|
188
|
+
it 'does not raise an error when a field is not present' do
|
189
|
+
expect(sample_response.co_author).to be_nil
|
190
|
+
expect(sample_response.other_author).to be_nil
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'when the optional field is present' do
|
194
|
+
let(:sample_response_payload) do
|
195
|
+
super().merge(co_author: { handle: 'dave' })
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'exposes the field' do
|
199
|
+
expect(sample_response.co_author.username).to eql('dave')
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe OpenAI::Tokenizer do
|
4
|
+
let(:tokenizer) { described_class.new }
|
5
|
+
|
6
|
+
it 'can get an encoder by model name' do
|
7
|
+
expect(tokenizer.for_model('gpt-4')).to eql(
|
8
|
+
OpenAI::Tokenizer::Encoding.new(:cl100k_base)
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'can get an encoder by name' do
|
13
|
+
expect(tokenizer.get(:cl100k_base)).to eql(
|
14
|
+
OpenAI::Tokenizer::Encoding.new(:cl100k_base)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises an error if the model name is not valid' do
|
19
|
+
expect { tokenizer.for_model('gpt-42') }.to raise_error(
|
20
|
+
'Invalid model name or not recognized by Tiktoken: "gpt-42"'
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'raises an error if the encoding name is not valid' do
|
25
|
+
expect { tokenizer.get('aaaaaaaaaaaa') }.to raise_error(
|
26
|
+
'Invalid encoding name or not recognized by Tiktoken: "aaaaaaaaaaaa"'
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'can encode text' do
|
31
|
+
expect(tokenizer.for_model('gpt-4').encode('Hello, world!')).to eql(
|
32
|
+
[9906, 11, 1917, 0]
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'can decode tokens' do
|
37
|
+
expect(tokenizer.for_model('gpt-4').decode([9906, 11, 1917, 0])).to eql(
|
38
|
+
'Hello, world!'
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'can count the number of tokens in text' do
|
43
|
+
expect(tokenizer.for_model('gpt-4').num_tokens('Hello, world!')).to eql(4)
|
44
|
+
end
|
45
|
+
end
|