File Coverage

blib/lib/OpenAI/API/Request.pm
Criterion Covered Total %
statement 73 116 62.9
branch 7 28 25.0
condition 1 6 16.6
subroutine 20 31 64.5
pod 4 5 80.0
total 105 186 56.4


line stmt bran cond sub pod time code
1             package OpenAI::API::Request;
2              
3 18     18   26837 use IO::Async::Loop;
  18         367634  
  18         628  
4 18     18   8435 use IO::Async::Future;
  18         240464  
  18         511  
5 18     18   7590 use JSON::MaybeXS;
  18         94109  
  18         1219  
6 18     18   12833 use LWP::UserAgent;
  18         870244  
  18         758  
7              
8 18     18   186 use Moo;
  18         45  
  18         225  
9 18     18   8167 use strictures 2;
  18         185  
  18         897  
10 18     18   4438 use namespace::clean;
  18         54  
  18         201  
11              
12 18     18   6908 use OpenAI::API::Config;
  18         56  
  18         468  
13 18     18   8864 use OpenAI::API::Error;
  18         66  
  18         30892  
14              
15             has 'config' => (
16             is => 'ro',
17             default => sub { OpenAI::API::Config->new() },
18             isa => sub {
19             die "config must be an instance of OpenAI::API::Config"
20             unless ref $_[0] eq 'OpenAI::API::Config';
21             },
22             coerce => sub {
23             return $_[0] if ref $_[0] eq 'OpenAI::API::Config';
24             return OpenAI::API::Config->new( %{ $_[0] } );
25             },
26             );
27              
28             has 'user_agent' => (
29             is => 'ro',
30             lazy => 1,
31             builder => '_build_user_agent',
32             );
33              
34             has 'event_loop' => (
35             is => 'ro',
36             lazy => 1,
37             builder => '_build_event_loop',
38             );
39              
40             sub _build_user_agent {
41 4     4   80 my ($self) = @_;
42 4         76 $self->{user_agent} = LWP::UserAgent->new( timeout => $self->config->timeout );
43             }
44              
45             sub _build_event_loop {
46 4     4   73 my ($self) = @_;
47 4         24 my $class = $self->config->event_loop_class;
48 4 50       234 eval "require $class" or die "Failed to load event loop class $class: $@";
49 4         28 return $class->new();
50             }
51              
52             sub endpoint {
53 0     0 1 0 die "Must be implemented";
54             }
55              
56             sub method {
57 0     0 1 0 die "Must be implemented";
58             }
59              
60             sub _parse_response {
61 0     0   0 my ( $self, $res ) = @_;
62              
63 0   0     0 my $class = ref $self || $self;
64              
65             # Replace s/Request/Response/ to find the response module
66 0         0 ( my $response_module = $class ) =~ s/Request/Response/;
67              
68             # Require the OpenAI::API::Response module
69 0 0       0 eval "require $response_module" or die $@;
70              
71             # Return the OpenAI::API::Response object
72 0         0 my $decoded_res = decode_json( $res->decoded_content );
73 0         0 return $response_module->new($decoded_res);
74             }
75              
76             sub request_params {
77 4     4 0 14 my ($self) = @_;
78 4         7 my %request_params = %{$self};
  4         21  
79 4         11 delete $request_params{config};
80 4         9 delete $request_params{user_agent};
81 4         9 delete $request_params{event_loop};
82 4         55 return \%request_params;
83             }
84              
85             sub send {
86 4     4 1 16 my $self = shift;
87              
88 4 50       18 if ( @_ == 1 ) {
89 0         0 warn "Sending config via send is deprecated. More info: perldoc OpenAI::API::Config\n";
90             }
91              
92 4         14 my %args = @_;
93              
94 4 0       23 my $res =
    50          
95             $self->method eq 'POST' ? $self->_post()
96             : $self->method eq 'GET' ? $self->_get()
97             : die "Invalid method";
98              
99 0 0       0 if ( $args{http_response} ) {
100 0         0 return $res;
101             }
102              
103 0         0 return $self->_parse_response($res);
104             }
105              
106             sub _get {
107 0     0   0 my ($self) = @_;
108              
109 0         0 my $req = $self->_create_request('GET');
110 0         0 return $self->_send_request($req);
111             }
112              
113             sub _post {
114 4     4   19 my ($self) = @_;
115              
116 4         26 my $req = $self->_create_request( 'POST', encode_json( $self->request_params() ) );
117 4         33 return $self->_send_request($req);
118             }
119              
120             sub send_async {
121 0     0 1 0 my ( $self, %args ) = @_;
122              
123 0 0       0 my $res_future =
    0          
124             $self->method eq 'POST' ? $self->_post_async()
125             : $self->method eq 'GET' ? $self->_get_async()
126             : die "Invalid method";
127              
128 0 0       0 if ( $args{http_response} ) {
129 0         0 return $res_future;
130             }
131              
132             # Return a new future that resolves to $res->decoded_content
133             my $decoded_content_future = $res_future->then(
134             sub {
135 0     0   0 my $res = shift;
136 0         0 return $self->_parse_response($res);
137             }
138 0         0 );
139              
140 0         0 return $decoded_content_future;
141             }
142              
143             sub _get_async {
144 0     0   0 my ($self) = @_;
145              
146 0         0 my $req = $self->_create_request('GET');
147 0         0 return $self->_send_request_async($req);
148             }
149              
150             sub _post_async {
151 0     0   0 my ( $self, $config ) = @_;
152              
153 0         0 my $req = $self->_create_request( 'POST', encode_json( $self->request_params() ) );
154 0         0 return $self->_send_request_async($req);
155             }
156              
157             sub _create_request {
158 4     4   15 my ( $self, $method, $content ) = @_;
159              
160 4         109 my $req = HTTP::Request->new(
161             $method => $self->config->api_base . "/" . $self->endpoint,
162             $self->_request_headers(),
163             $content,
164             );
165              
166 4         9517 return $req;
167             }
168              
169             sub _request_headers {
170 4     4   15 my ($self) = @_;
171              
172             return [
173 4         82 'Content-Type' => 'application/json',
174             'Authorization' => 'Bearer ' . $self->config->api_key,
175             ];
176             }
177              
178             sub _send_request {
179 4     4   15 my ( $self, $req ) = @_;
180              
181 4         43 my $loop = IO::Async::Loop->new();
182              
183 4         6014 my $future = $self->_async_http_send_request($req);
184              
185 4         26 $loop->await($future);
186              
187 4         551 my $res = $future->get;
188              
189 4 50       101 if ( !$res->is_success ) {
190 4         39 OpenAI::API::Error->throw(
191 4         21 message => "Error: '@{[ $res->status_line ]}'",
192             request => $req,
193             response => $res,
194             );
195             }
196              
197 0         0 return $res;
198             }
199              
200             sub _send_request_async {
201 0     0   0 my ( $self, $req ) = @_;
202              
203             return $self->_async_http_send_request($req)->then(
204             sub {
205 0     0   0 my $res = shift;
206              
207 0 0       0 if ( !$res->is_success ) {
208 0         0 OpenAI::API::Error->throw(
209 0         0 message => "Error: '@{[ $res->status_line ]}'",
210             request => $req,
211             response => $res,
212             );
213             }
214              
215 0         0 return $res;
216             }
217             )->catch(
218             sub {
219 0     0   0 my $err = shift;
220 0         0 die $err;
221             }
222 0         0 );
223             }
224              
225             sub _http_send_request {
226 4     4   17 my ( $self, $req ) = @_;
227              
228 4         111 for my $attempt ( 1 .. $self->config->retry ) {
229 4         149 my $res = $self->user_agent->request($req);
230              
231 4 50 33     911974 if ( $res->is_success ) {
    50          
232 0         0 return $res;
233             } elsif ( $res->code =~ /^(?:500|503|504|599)$/ && $attempt < $self->config->retry ) {
234 0         0 sleep( $self->config->sleep );
235             } else {
236 4         149 return $res;
237             }
238             }
239             }
240              
241             sub _async_http_send_request {
242 4     4   15 my ( $self, $req ) = @_;
243              
244 4         31 my $future = IO::Async::Future->new;
245              
246             $self->event_loop->later(
247             sub {
248             eval {
249 4         28 my $res = $self->_http_send_request($req);
250 4         56 $future->done($res);
251 4         258 1;
252 4 50   4   503 } or do {
253 0         0 my $err = $@;
254 0         0 $future->fail($err);
255             };
256             }
257 4         263 );
258              
259 4         182 return $future;
260             }
261              
262             1;
263              
264             __END__
265              
266             =head1 NAME
267              
268             OpenAI::API::Request - Base module for making requests to the OpenAI API
269              
270             =head1 SYNOPSIS
271              
272             This module is a base module for making HTTP requests to the OpenAI
273             API. It should not be used directly.
274              
275             package OpenAI::API::Request::NewRequest;
276             use Moo;
277             extends 'OpenAI::API::Request';
278              
279             sub endpoint {
280             '/my_endpoint'
281             }
282              
283             sub method {
284             'POST'
285             }
286              
287             # somewhere else...
288              
289             use OpenAI::API::Request::NewRequest;
290              
291             my $req = OpenAI::API::Request::NewRequest->new(...);
292              
293             my $res = $req->send(); # or: my $res = $req->send_async();
294              
295             =head1 DESCRIPTION
296              
297             This module provides a base class for creating request objects for the
298             OpenAI API. It includes methods for sending synchronous and asynchronous
299             requests, with support for HTTP GET and POST methods.
300              
301             =head1 ATTRIBUTES
302              
303             =over 4
304              
305             =item * config
306              
307             An instance of L<OpenAI::API::Config> that provides configuration
308             options for the OpenAI API client. Defaults to a new instance of
309             L<OpenAI::API::Config>.
310              
311             =item * user_agent
312              
313             An instance of L<LWP::UserAgent> that is used to make HTTP
314             requests. Defaults to a new instance of L<LWP::UserAgent> with a timeout
315             set to the value of C<config-E<gt>timeout>.
316              
317             =back
318              
319             =head1 METHODS
320              
321             =head2 endpoint
322              
323             This method must be implemented by subclasses. It should return the API
324             endpoint for the specific request.
325              
326             =head2 method
327              
328             This method must be implemented by subclasses. It should return the HTTP
329             method for the specific request.
330              
331             =head2 send
332              
333             Send a request synchronously.
334              
335             my $response = $request->send();
336              
337             =head2 send_async
338              
339             Send a request asynchronously. Returns a future that will be resolved
340             with the decoded JSON response.
341              
342             Here's an example usage:
343              
344             use IO::Async::Loop;
345              
346             my $loop = IO::Async::Loop->new();
347              
348             my $future = $request->send_async()->then(
349             sub {
350             my $content = shift;
351             # ...
352             }
353             )->catch(
354             sub {
355             my $error = shift;
356             # ...
357             }
358             );
359              
360             $loop->await($future);
361              
362             my $res = $future->get;
363              
364             Note: You can select different event loops via L<OpenAI::API::Config>.