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   28395 use IO::Async::Loop;
  18         359244  
  18         633  
4 18     18   8099 use IO::Async::Future;
  18         237410  
  18         483  
5 18     18   7608 use JSON::MaybeXS;
  18         94426  
  18         1293  
6 18     18   13117 use LWP::UserAgent;
  18         864940  
  18         684  
7              
8 18     18   154 use Moo;
  18         50  
  18         193  
9 18     18   8107 use strictures 2;
  18         206  
  18         854  
10 18     18   4154 use namespace::clean;
  18         43  
  18         211  
11              
12 18     18   6596 use OpenAI::API::Config;
  18         53  
  18         442  
13 18     18   8388 use OpenAI::API::Error;
  18         63  
  18         31235  
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   67 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   66 my ($self) = @_;
47 4         24 my $class = $self->config->event_loop_class;
48 4 50       239 eval "require $class" or die "Failed to load event loop class $class: $@";
49 4         26 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 15 my ($self) = @_;
78 4         9 my %request_params = %{$self};
  4         23  
79 4         12 delete $request_params{config};
80 4         10 delete $request_params{user_agent};
81 4         7 delete $request_params{event_loop};
82 4         54 return \%request_params;
83             }
84              
85             sub send {
86 4     4 1 13 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         12 my %args = @_;
93              
94 4 0       20 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   17 my ($self) = @_;
115              
116 4         26 my $req = $self->_create_request( 'POST', encode_json( $self->request_params() ) );
117 4         37 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   16 my ( $self, $method, $content ) = @_;
159              
160 4         110 my $req = HTTP::Request->new(
161             $method => $self->config->api_base . "/" . $self->endpoint,
162             $self->_request_headers(),
163             $content,
164             );
165              
166 4         9308 return $req;
167             }
168              
169             sub _request_headers {
170 4     4   12 my ($self) = @_;
171              
172             return [
173 4         74 'Content-Type' => 'application/json',
174             'Authorization' => 'Bearer ' . $self->config->api_key,
175             ];
176             }
177              
178             sub _send_request {
179 4     4   14 my ( $self, $req ) = @_;
180              
181 4         39 my $loop = IO::Async::Loop->new();
182              
183 4         6082 my $future = $self->_async_http_send_request($req);
184              
185 4         20 $loop->await($future);
186              
187 4         682 my $res = $future->get;
188              
189 4 50       111 if ( !$res->is_success ) {
190 4         58 OpenAI::API::Error->throw(
191 4         23 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   16 my ( $self, $req ) = @_;
227              
228 4         135 for my $attempt ( 1 .. $self->config->retry ) {
229 4         131 my $res = $self->user_agent->request($req);
230              
231 4 50 33     869177 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         173 return $res;
237             }
238             }
239             }
240              
241             sub _async_http_send_request {
242 4     4   14 my ( $self, $req ) = @_;
243              
244 4         32 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         182 $future->done($res);
251 4         381 1;
252 4 50   4   496 } or do {
253 0         0 my $err = $@;
254 0         0 $future->fail($err);
255             };
256             }
257 4         272 );
258              
259 4         192 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             =head2 config
304              
305             An instance of L<OpenAI::API::Config> that provides configuration
306             options for the OpenAI API client. Defaults to a new instance of
307             L<OpenAI::API::Config>.
308              
309             =head2 user_agent
310              
311             An instance of L<LWP::UserAgent> that is used to make HTTP
312             requests. Defaults to a new instance of L<LWP::UserAgent> with a timeout
313             set to the value of C<config-E<gt>timeout>.
314              
315             =head2 event_loop
316              
317             An instance of an event loop class, specified by
318             C<config-E<gt>event_loop_class> option.
319              
320             =head1 METHODS
321              
322             =head2 endpoint
323              
324             This method must be implemented by subclasses. It should return the API
325             endpoint for the specific request.
326              
327             =head2 method
328              
329             This method must be implemented by subclasses. It should return the HTTP
330             method for the specific request.
331              
332             =head2 send(%args)
333              
334             This method sends the request and returns the parsed response. If the
335             'http_response' key is present in the %args hash, it returns the raw
336             L<HTTP::Response> object instead of the parsed response.
337              
338             my $response = $request->send();
339              
340             my $response = $request->send( http_response => 1 );
341              
342             =head2 send_async(%args)
343              
344             This method sends the request asynchronously and returns a
345             L<IO::Async::Future> object. If the 'http_response' key is present in
346             the %args hash, it resolves to the raw L<HTTP::Response> object instead
347             of the parsed response.
348              
349             Here's an example usage:
350              
351             use IO::Async::Loop;
352              
353             my $loop = IO::Async::Loop->new();
354              
355             my $future = $request->send_async()->then(
356             sub {
357             my $content = shift;
358             # ...
359             }
360             )->catch(
361             sub {
362             my $error = shift;
363             # ...
364             }
365             );
366              
367             $loop->await($future);
368              
369             my $res = $future->get;
370              
371             Note: if you want to use a different event loop, you must pass it via
372             the L<config|OpenAI::API::Config> attribute.