diff --git a/lib/OpenAIAsync/Server.pm b/lib/OpenAIAsync/Server.pm new file mode 100644 index 0000000..c808c18 --- /dev/null +++ b/lib/OpenAIAsync/Server.pm @@ -0,0 +1,319 @@ +package OpenAIAsync::Server; + +use v5.36.0; +use Object::Pad; +use IO::Async::SSL; # We're not directly using it but I want to enforce that we pull it in when detecting dependencies, since openai itself is always https +use Future::AsyncAwait; +use IO::Async; + +use OpenAIAsync::Types::Results; +use OpenAIAsync::Types::Requests; + +our $VERSION = '0.02'; + +# ABSTRACT: Async server for OpenAI style REST API for various AI systems (LLMs, Images, Video, etc.) + +=pod + +=head1 NAME + +OpenAIAsync::Server - IO::Async based server for OpenAI compatible APIs + +=head1 SYNOPSIS + + use IO::Async::Loop; + use OpenAIAsync::Server; + use builtin qw/true false/; + + my $loop = IO::Async::Loop->new(); + + class MyServer { + inherit OpenAIAsync::Server; + + method init() { + # We return the info on where we should be listening, and any other settings for Net::Async::HTTP::Server + + return [ + { + port => "8085", + listen => "127.0.0.1", + ... + }, + ... + ]; + } + + async method auth_check($key, $http_req) { + # we can implement any auth checks that we need here + return true; # AIs need to be free! + } + + async method chat_completion($chat_completion_req) { + ... + + return $chat_completion_response; + } + + ... + } + + my $server = MyServer->new(); + + $loop->add($server); + + $loop->run() + +=head1 THEORY OF OPERATION + +This module implements the L interface, this means that you create a new server and then call C<< $loop->add($client) >> +this casues all Ls that are created to be part of the L of your program. This way when you call C on any method +it will properly suspend the execution of your program and do something else concurrently (probably waiting on requests). + +All the methods that you provide must be async, this is so that multiple requests can be easily handled internally with ease. + +You subclass the ::Server class and implement your own methods for handling the limited number of requests, and provide some +configuration information for the servers to start in the init() method + + +=head1 Methods + +=head2 new() + +Create a new OpenAIAsync::Server. You'll need to register the server with C<< $loop->add($server) >> after creation. + +=head3 PARAMETERS + +=over 4 + +=item * ? + +TODO FILL IN + +=back + +=head2 auth_check($key, $http_req) + +This method requres async keyword. + +Return a true or false value on if a request should be authorized. This is given the API-Key value from the Authorization header + +if $key is undef then you can use the $http_req object to get a look at the full headers, so you can implement whatever other authorization system you need + +By default all OpenAI compatible APIs are expecting ONLY an API-Key type Authorization header so doing anytyhing else isn't strictly compatible but there's no reason that this server shouldn't be able to do more if you're customizing things + +=head2 completion (deprecated) + +This method requres async keyword. + +Handle a completion request, takes in a request object, must return a response object + +=head2 chat + +This method requres async keyword. + +Handle a chat completion request + +=head2 embedding + +This method requres async keyword. + +Handle an embedding request + +=head2 image_generate + +This method requres async keyword. + +Unimplemented, but once present will be used to generate images with Dall-E (or for self hosted, stable diffusion). + +=head2 text_to_speech + +This method requres async keyword. + +Unimplemented, but can be used to turn text to speech using whatever algorithms/models are supported. + +=head2 speech_to_text + +This method requres async keyword. + +Unimplemented. The opposite of the above. + +=head2 vision + +This method requres async keyword. + +Unimplemented, I've not investigated this one much yet but I believe it's to get a description of an image and it's contents. + +=head2 Missing apis + +At least some for getting the list of models and some other meta information, those will be added next after I get some more documentation written + +=head1 See Also + +L, L, L + +=head1 License + +Artistic 2.0 + +=head1 Author + +Ryan Voots, ... etc. + +=cut + +class OpenAIAsync::Client :repr(HASH) :isa(IO::Async::Notifier) :strict(params) { + use JSON::MaybeXS qw//; + use Net::Async::HTTP; + use Feature::Compat::Try; + use URI; + + field $_json = JSON::MaybeXS->new(utf8 => 1, convert_blessed => 1); + field $http; + + # TODO document these directly, other options gets mixed in BEFORE all of these + field $_http_max_in_flight :param(http_max_in_flight) = 2; + field $_http_max_redirects :param(http_max_redirects) = 3; + field $_http_max_connections_per_host :param(http_max_connections_per_host) = 2; + field $_http_timeout :param(http_timeout) = 120; # My personal server is kinda slow, use a generous default + field $_http_stall_timeout :param(http_stall_timeout) = 600; # generous for my slow personal server + field $_http_other :param(http_other_options) = {}; + field $_http_user_agent :param(http_user_agent) = __PACKAGE__." Perl/$VERSION (Net::Async::HTTP/".$Net::Async::HTTP::VERSION." IO::Async/".$IO::Async::VERSION." Perl/$])"; + + field $api_base :param(api_base) = $ENV{OPENAI_API_BASE} // "https://api.openai.com/v1"; + field $api_key :param(api_key) = $ENV{OPENAI_API_KEY}; + + field $api_org_name :param(api_org_name) = undef; + + field $io_async_notifier_params :param = undef; + + method configure(%params) { + # We require them to go this way, so that there is no conflicts + # TODO document this + my %io_async_params = ($params{io_async_notifier_params} // {})->%*; + IO::Async::Notifier::configure($self, %io_async_params); + } + + method __make_http() { + die "Missing API Key for OpenAI" unless $api_key; + + return Net::Async::HTTP->new( + $_http_other->%*, + user_agent => "SNN OpenAI Client 1.0", + +headers => { + "Authorization" => "Bearer $api_key", + "Content-Type" => "application/json", + $api_org_name ? ( + 'OpenAI-Organization' => $api_org_name, + ) : () + }, + max_redirects => $_http_max_redirects, + max_connections_per_host => $_http_max_connections_per_host, + max_in_flight => $_http_max_in_flight, + timeout => $_http_timeout, + stall_timeout => $_http_stall_timeout, + ) + } + + ADJUST { + $http = $self->__make_http; + + $api_base =~ s|/$||; # trim an accidental final / since we will be putting it on the endpoints + } + + async method _make_request($endpoint, $data) { + my $json = $_json->encode($data); + + my $url = URI->new($api_base . $endpoint ); + + my $result = await $http->do_request( + uri => $url, + method => "POST", + content => $json, + content_type => 'application/json', + ); + + if ($result->is_success) { + my $json = $result->decoded_content; + my $out_data = $_json->decode($json); + + return $out_data; + } else { + die "Failure in talking to OpenAI service: ".$result->status_line.": ".$result->decoded_content; + } + } + + method _add_to_loop($loop) { + $loop->add($http); + } + + method _remove_from_loop($loop) { + $loop->remove($http); + $http = $self->__make_http; # overkill? want to make sure we have a clean one + } + + # This is the legacy completion api + async method completion($input) { + + if (ref($input) eq 'HASH') { + $input = OpenAIAsync::Types::Requests::Completion->new($input->%*); + } elsif (ref($input) eq 'OpenAIAsync::Types::Requests::Completion') { + # dummy, nothing to do + } else { + die "Unsupported input type [".ref($input)."]"; + } + + my $data = await $self->_make_request($input->_endpoint(), $input); + + my $type_result = OpenAIAsync::Types::Results::Completion->new($data->%*); + + return $type_result; + } + + async method chat($input) { + if (ref($input) eq 'HASH') { + $input = OpenAIAsync::Types::Requests::ChatCompletion->new($input->%*); + } elsif (ref($input) eq 'OpenAIAsync::Types::Requests::ChatCompletion') { + # dummy, nothing to do + } else { + die "Unsupported input type [".ref($input)."]"; + } + + my $data = await $self->_make_request($input->_endpoint(), $input); + + my $type_result = OpenAIAsync::Types::Results::ChatCompletion->new($data->%*); + + return $type_result; + } + + async method embedding($input) { + if (ref($input) eq 'HASH') { + $input = OpenAIAsync::Types::Requests::Embedding->new($input->%*); + } elsif (ref($input) eq 'OpenAIAsync::Types::Requests::Embedding') { + # dummy, nothing to do + } else { + die "Unsupported input type [".ref($input)."]"; + } + + my $data = await $self->_make_request($input->_endpoint(), $input); + + my $type_result = OpenAIAsync::Types::Results::Embedding->new($data->%*); + + return $type_result; + } + + async method image_generate($input) { + ... + } + + async method text_to_speech($text) { + ... + } + + async method speech_to_text($sound_data) { + ... + } + + async method vision($image, $prompt) { + ... + } +} \ No newline at end of file