File Coverage

blib/lib/OpenAI/API/Request.pm
Criterion Covered Total %
statement 68 112 60.7
branch 6 26 23.0
condition 1 6 16.6
subroutine 20 32 62.5
pod 4 5 80.0
total 99 181 54.7


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