Skip to content

Commit

Permalink
Refactor streaming support and add test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
allantatter committed Jul 4, 2024
1 parent daed4cf commit 1cc5b26
Show file tree
Hide file tree
Showing 46 changed files with 574 additions and 120 deletions.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"outl1ne/nova-translations-loader": "^5.0"
},
"require-dev": {
"nova-kit/nova-devtool": "^1.5"
"nova-kit/nova-devtool": "^1.5",
"phpunit/phpunit": "^11.1"
},
"autoload": {
"psr-4": {
Expand Down Expand Up @@ -65,4 +66,4 @@
"@php vendor/bin/phpstan analyse"
]
}
}
}
34 changes: 34 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
<php>
<env name="APP_KEY" value="base64:cUeL2FRY3BrH2xhgsIjnpQVb22EN49E37/XOtuQitiY="/>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_STORE" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>
11 changes: 11 additions & 0 deletions src/Cache/CacheInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Outl1ne\NovaOpenAI\Cache;

use Outl1ne\NovaOpenAI\Capabilities\Responses\Response;

interface CacheInterface
{
public function get(array $arguments, callable $callback);
public function put(array $arguments, Response $result);
}
8 changes: 5 additions & 3 deletions src/Cache/EmbeddingsCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
namespace Outl1ne\NovaOpenAI\Cache;

use Illuminate\Support\Facades\Cache;
use Outl1ne\NovaOpenAI\Capabilities\Embeddings\Responses\CachedEmbeddingsResponse;
use Outl1ne\NovaOpenAI\Capabilities\Responses\Response;
use Outl1ne\NovaOpenAI\Capabilities\Embeddings\Responses\EmbeddingsResponse;
use Outl1ne\NovaOpenAI\Capabilities\Embeddings\Responses\CachedEmbeddingsResponse;

class EmbeddingsCache
class EmbeddingsCache implements CacheInterface
{
public function __construct(readonly bool $enabled)
{
Expand All @@ -25,8 +26,9 @@ public function get(array $arguments, callable $callback)
return $callback($arguments);
}

public function put(array $arguments, EmbeddingsResponse $result)
public function put(array $arguments, Response $result)
{
if (!$result instanceof EmbeddingsResponse) return;
if (!$this->enabled) return;

Cache::put($this->cacheKey($arguments['model'], $arguments['input'], $arguments), [
Expand Down
10 changes: 7 additions & 3 deletions src/Capabilities/Assistants/CreateAssistant.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants", [
...$this->request->arguments,
$response = $this->openAI->http()->post("assistants", [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
...$this->request->arguments,
]),
]);
$response->throw();

return $this->handleResponse(new AssistantResponse($response));
} catch (Exception $e) {
Expand Down
10 changes: 7 additions & 3 deletions src/Capabilities/Assistants/CreateAssistantFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants/{$assistantId}/files", [
...$this->request->arguments,
$response = $this->openAI->http()->post("assistants/{$assistantId}/files", [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
...$this->request->arguments,
]),
]);
$response->throw();

return $this->handleResponse(new AssistantFileResponse($response));
} catch (Exception $e) {
Expand Down
10 changes: 7 additions & 3 deletions src/Capabilities/Assistants/DeleteAssistant.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("assistants/{$assistantId}", [
...$this->request->arguments,
$response = $this->openAI->http()->delete("assistants/{$assistantId}", [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
...$this->request->arguments,
]),
]);
$response->throw();

return $this->handleResponse(new DeleteResponse($response));
} catch (Exception $e) {
Expand Down
7 changes: 5 additions & 2 deletions src/Capabilities/Assistants/DeleteAssistantFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("assistants/{$assistantId}/files/{$fileId}");
$response->throw();
$response = $this->openAI->http()->delete("assistants/{$assistantId}/files/{$fileId}", [
'headers' => [
'Content-Type' => 'application/json',
],
]);

return $this->handleResponse(new DeleteResponse($response));
} catch (Exception $e) {
Expand Down
7 changes: 4 additions & 3 deletions src/Capabilities/Assistants/ListAssistantFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("assistants/{$assistantId}/files", [
...$this->request->arguments,
$response = $this->openAI->http()->get("assistants/{$assistantId}/files", [
'query' => [
...$this->request->arguments,
],
]);
$response->throw();

return $this->handleResponse(new AssistantFileListResponse($response));
} catch (Exception $e) {
Expand Down
10 changes: 7 additions & 3 deletions src/Capabilities/Assistants/ModifyAssistant.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post("assistants/{$assistantId}", [
...$this->request->arguments,
$response = $this->openAI->http()->post("assistants/{$assistantId}", [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
...$this->request->arguments,
]),
]);
$response->throw();

return $this->handleResponse(new AssistantResponse($response));
} catch (Exception $e) {
Expand Down
2 changes: 2 additions & 0 deletions src/Capabilities/Capability.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Outl1ne\NovaOpenAI\OpenAI;
use Outl1ne\NovaOpenAI\Cache\CacheInterface;

class Capability
{
Expand All @@ -12,6 +13,7 @@ class Capability
public Closure $shouldStoreErrorsCallback;
public Closure $storingCallback;
public ?Closure $streamCallback = null;
public ?CacheInterface $cache;

public function __construct(
public readonly OpenAI $openAI,
Expand Down
28 changes: 17 additions & 11 deletions src/Capabilities/CapabilityClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
use Closure;
use Exception;
use Outl1ne\NovaOpenAI\OpenAI;
use GuzzleHttp\Promise\Promise;
use Outl1ne\NovaOpenAI\StreamHandler;
use Psr\Http\Message\ResponseInterface;
use Outl1ne\NovaOpenAI\Pricing\Calculator;
use Outl1ne\NovaOpenAI\Models\OpenAIRequest;
use Illuminate\Http\Client\Response as HttpResponse;
use GuzzleHttp\Psr7\Response as HttpResponse;
use Outl1ne\NovaOpenAI\Capabilities\Responses\Response;
use Outl1ne\NovaOpenAI\Capabilities\Responses\CachedResponse;
use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamChunk;
use Outl1ne\NovaOpenAI\Capabilities\Responses\CachedResponse;

abstract class CapabilityClient
{
Expand Down Expand Up @@ -80,20 +82,24 @@ protected function isStreamedResponse(HttpResponse $response)
return strpos($response->getHeaderLine('Content-Type'), 'text/event-stream') !== false;
}

protected function handleStreamedResponse(HttpResponse $httpResponse, ?callable $handleResponse = null)
protected function handleStreamedResponse(Promise $promise, ?callable $handleResponse = null)
{
if (!$this->capability->streamCallback instanceof Closure) {
throw new Exception('Response is a stream but stream callback is not defined.');
}

$response = (new StreamHandler($httpResponse, $this->capability->streamCallback, function (StreamChunk $streamChunk) {
$this->request->status = 'streaming';
$this->request->meta = $streamChunk?->meta;
$this->request->model_used = $streamChunk?->model;
if (($this->capability->shouldStoreCallback)($streamChunk)) {
$this->request->save();
}
}))->handle();
$chainedPromise = $promise->then(function (ResponseInterface $stream) {
$response = (new StreamHandler($stream, $this->capability->streamCallback, function (StreamChunk $streamChunk) {
$this->request->status = 'streaming';
$this->request->meta = $streamChunk?->meta;
$this->request->model_used = $streamChunk?->model;
if (($this->capability->shouldStoreCallback)($streamChunk)) {
$this->request->save();
}
}))->handle();
return $response;
});
$response = $chainedPromise->wait();

$this->request->cost = $this->calculateCost($response);
$this->request->time_sec = $this->measure();
Expand Down
39 changes: 31 additions & 8 deletions src/Capabilities/Chat/CreateChat.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
namespace Outl1ne\NovaOpenAI\Capabilities\Chat;

use Exception;
use Illuminate\Http\Client\Response;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Response as GuzzleResponse;
use Outl1ne\NovaOpenAI\Capabilities\CapabilityClient;
use Outl1ne\NovaOpenAI\Capabilities\Chat\Parameters\Messages;
use Outl1ne\NovaOpenAI\Capabilities\Chat\Responses\ChatResponse;
Expand Down Expand Up @@ -54,14 +59,32 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->post('chat/completions', [
'model' => $model,
'messages' => $messages->messages,
...$this->request->arguments,
]);
$response->throw();
if (isset($this->request->arguments['stream'])) {
$response = $this->openAI->http()->postAsync('chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
'model' => $model,
'messages' => $messages->messages,
...$this->request->arguments,
]),
'stream' => true,
]);
} else {
$response = $this->openAI->http()->post('chat/completions', [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
'model' => $model,
'messages' => $messages->messages,
...$this->request->arguments,
]),
]);
}

if ($this->isStreamedResponse($response)) {
if ($response instanceof Promise) {
return $this->handleStreamedResponse($response, [$this, 'response']);
}
return $this->handleResponse(new ChatResponse($response), [$this, 'response']);
Expand Down
2 changes: 1 addition & 1 deletion src/Capabilities/Chat/Responses/StreamedChatResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Outl1ne\NovaOpenAI\Capabilities\Chat\Responses;

use Illuminate\Http\Client\Response;
use GuzzleHttp\Psr7\Response;
use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamChunk;
use Outl1ne\NovaOpenAI\Capabilities\Responses\StreamResponse;

Expand Down
9 changes: 6 additions & 3 deletions src/Capabilities/Embeddings/CreateEmbedding.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,16 @@ public function makeRequest(string $model, string $input, ?string $encodingForma
'model' => $model,
'input' => $input,
...$this->request->arguments,
], fn (...$args) => $this->openAI->http()->withHeader('Content-Type', 'application/json')->post('embeddings', ...$args));
], fn ($args) => $this->openAI->http()->post('embeddings', [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode($args),
]));

if ($response instanceof CachedEmbeddingsResponse) {
return $this->handleCachedResponse($response, [$this, 'cachedResponse']);
} else {
$response->throw();

$result = $this->handleResponse(new EmbeddingsResponse($response), [$this, 'response']);

$this->capability->cache->put([
Expand Down
2 changes: 0 additions & 2 deletions src/Capabilities/Embeddings/Embeddings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

class Embeddings extends Capability
{
public EmbeddingsCache $cache;

public function create(string $model, string $input, ?string $encodingFormat = null, ?int $dimensions = null, ?string $user = null)
{
return (new CreateEmbedding($this))->makeRequest($model, $input, $encodingFormat, $dimensions, $user);
Expand Down
16 changes: 10 additions & 6 deletions src/Capabilities/Files/DeleteFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@

use Exception;
use Outl1ne\NovaOpenAI\Capabilities\CapabilityClient;
use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\DeleteResponse;
use Outl1ne\NovaOpenAI\Capabilities\Files\Responses\FileDeleteResponse;

class DeleteFile extends CapabilityClient
{
protected string $method = 'files';

public function makeRequest(
string $fileId,
): DeleteResponse {
): FileDeleteResponse {
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->delete("files/{$fileId}", [
...$this->request->arguments,
$response = $this->openAI->http()->delete("files/{$fileId}", [
'headers' => [
'Content-Type' => 'application/json',
],
'body' => json_encode([
...$this->request->arguments,
]),
]);
$response->throw();

return $this->handleResponse(new DeleteResponse($response));
return $this->handleResponse(new FileDeleteResponse($response));
} catch (Exception $e) {
$this->handleException($e);
}
Expand Down
7 changes: 4 additions & 3 deletions src/Capabilities/Files/ListFiles.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ public function makeRequest(
$this->pending();

try {
$response = $this->openAI->http()->withHeader('Content-Type', 'application/json')->get("files", [
...$this->request->arguments,
$response = $this->openAI->http()->get("files", [
'query' => [
...$this->request->arguments,
],
]);
$response->throw();

return $this->handleResponse(new FileListResponse($response));
} catch (Exception $e) {
Expand Down
Loading

0 comments on commit 1cc5b26

Please sign in to comment.