Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions config/openai.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,15 @@
*/

'request_timeout' => env('OPENAI_REQUEST_TIMEOUT', 30),

/*
|--------------------------------------------------------------------------
| HTTP Handler
|--------------------------------------------------------------------------
|
| Define a custom HTTP handler (class-string or callable) for OpenAI requests.
| Useful for adding Laravel Http Events, logging, or retries via Guzzle HandlerStack.
| Base Handler: OpenAI\Laravel\Http\Handler::class
*/
'http_handler' => env('OPENAI_HTTP_HANDLER', null),
];
146 changes: 146 additions & 0 deletions src/Http/Handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

namespace OpenAI\Laravel\Http;

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Promise\PromiseInterface;
use OpenAI\Laravel\Http\Handlers\HttpEvent;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class Handler
{
/**
* The underlying handler used by the middleware stack.
*
* @var callable|null
*/
protected $handler = null;

/**
* Cached the handler stack.
*/
protected ?HandlerStack $handlerStack = null;

/**
* Indicates whether Laravel HTTP events should be dispatched globally.
*/
protected static bool $shouldEvent = true;

/**
* Whether Laravel HTTP events should be dispatched.
*/
public static function shouldEvent(bool $enabled = true): void
{
static::$shouldEvent = $enabled;
}

/**
* Determine if event dispatching is currently enabled.
*/
public static function isEventEnabled(): bool
{
return static::$shouldEvent;
}

/**
* Resolve the config-defined handler into a callable or null.
*
* @param mixed $handler The handler to resolve.
*/
public static function resolve($handler = null): ?callable
{
if (is_callable($handler)) {
return $handler;
}

if (is_string($handler) && is_callable($instance = app($handler))) {
return $instance;
}

return null;
}

/**
* Middleware that maps request failures (rejected promises) through a callback.
*
* @see https://docs.guzzlephp.org/en/stable/handlers-and-middleware.html#middleware
*/
public static function mapFailure(callable $fn): callable
{
return static function (callable $handler) use ($fn): callable {

return static function (RequestInterface $request, array $config) use ($handler, $fn) {

/** @var \GuzzleHttp\Promise\PromiseInterface $promise */
$promise = $handler($request, $config);

return $promise->then(null, onRejected: static function ($reason) use ($fn) {

$fn($reason); // side effects only

// continue rejection chain
return \GuzzleHttp\Promise\Create::rejectionFor($reason);
}
);
};
};
}

/**
* Set the underlying handler to be used by this middleware.
*/
public function withHandler(callable $handler): static
{
$this->handler = $handler;

return $this;
}

/**
* Get or create the handler stack.
*/
protected function getHandlerStack(): HandlerStack
{
return $this->handlerStack ??= HandlerStack::create($this->handler);
}

/**
* Invoke the handler stack with the given request and options.
*
* @param array<string, mixed> $config
* @return ResponseInterface|PromiseInterface
*/
public function __invoke(RequestInterface $request, array $config)
{
$handlerStack = $this->getHandlerStack();

// handler to dispatches the event / logger
if ($this->isEventEnabled()) {
$handlerStack->push(HttpEvent::request(), 'request-event');
$handlerStack->push(HttpEvent::failure(), 'failure-event');
$handlerStack->push(HttpEvent::response(), 'response-event');
}

// Pass the stack directly to handle()
return $this->handle($handlerStack, $request, $config);
}

/**
* Execute the handler stack with the given request and options.
*
* @param RequestInterface $request
* @param array<string, mixed> $config
* @return ResponseInterface|PromiseInterface
*/
public function handle(HandlerStack $handler, $request, array $config)
{
// Now you can push additional middleware directly
// Example:
// $stack->push(Middleware::mapRequest(fn(RequestInterface $request) => $request));

return $handler($request, $config);
}
}
118 changes: 118 additions & 0 deletions src/Http/Handlers/HttpEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace OpenAI\Laravel\Http\Handlers;

use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Promise\PromiseInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Http\Client\Request;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Event;
use OpenAI\Laravel\Http\Handler;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class HttpEvent
{
/**
* The underlying handler used by the middleware stack.
*
* @var callable|null
*/
private $handler = null;

/**
* Cached the handler stack.
*/
protected ?HandlerStack $handlerStack = null;

/**
* Set the underlying handler to be used by this middleware.
*/
public function withHandler(callable $handler): static
{
$this->handler = $handler;

return $this;
}

/**
* Get or create the handler stack.
*/
protected function getHandlerStack(): HandlerStack
{
return $this->handlerStack ??= HandlerStack::create($this->handler);
}

/**
* Handle the given HTTP request using a composed handler stack.
*
* @param array<string|int, mixed> $config
* @return ResponseInterface|PromiseInterface
*/
public function __invoke(RequestInterface $request, array $config = [])
{

$handlerStack = $this->getHandlerStack();

$handlerStack->push(static::request(), 'request-event');
$handlerStack->push(static::response(), 'response-event');
$handlerStack->push(static::failure(), 'failure-event');

return $handlerStack($request, $config);
}

/**
* Middleware to dispatch the Laravel HTTP RequestSending event before sending a request.
*
* @see https://api.laravel.com/docs/12.x/Illuminate/Http/Client/Events/RequestSending.html
*/
public static function request(): callable
{
return Middleware::tap(before: function (RequestInterface $request) {
Event::dispatch(new RequestSending(new Request($request)));
});
}

/**
* Middleware to dispatch the Laravel HTTP ResponseReceived event after a response is returned.
*
* @see https://api.laravel.com/docs/12.x/Illuminate/Http/Client/Events/ResponseReceived.html
*/
public static function response(): callable
{
return Middleware::tap(
after: function (RequestInterface $request, array $_o, PromiseInterface $promise) {
// $promise is the Response promise
$promise->then(function (ResponseInterface $response) use ($request) {
Event::dispatch(new ResponseReceived(
new Request($request),
new Response($response)
));
});
}
);
}

/**
* Middleware to dispatch the Laravel HTTP ConnectionFailed event on connection errors.
*
* @see https://api.laravel.com/docs/12.x/Illuminate/Http/Client/Events/ConnectionFailed.html
*/
public static function failure(): callable
{
return Handler::mapFailure(function ($e) {
if ($e instanceof ConnectException) {
$exception = new ConnectionException($e->getMessage(), $e->getCode());
Event::dispatch(new ConnectionFailed(new Request($e->getRequest()), $exception));
}
});
}
}
Loading