Search by

A unified PHP interface for LLM providers — Anthropic, OpenAI and Gemini — with streaming, tool calling, structured output and provider-agnostic middleware.

Package info

github.com/unknowingpro/php-ai

pkg:composer/unknowingpro/php-ai

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-29 10:10 UTC

This package is auto-updated.

Last update: 2026-08-29 10:21:46 UTC


README

Latest Version on Packagist

A unified PHP interface for Anthropic, OpenAI, Gemini, and OpenAI-compatible providers, with streaming, tool calling, structured output, embeddings, and provider-agnostic middleware.

Status

The core library, provider drivers, streaming adapters, middleware, testing kit, and Laravel integration are implemented. The current verification environment passes the automated suite; see Verification for the exact results and cross-version qualification.

Requirements

  • PHP 8.2 or newer
  • ext-json
  • A PSR-18 HTTP client and PSR-17 factories, or the bundled cURL transport (ext-curl)
  • Laravel 10.x, 11.x, or 12.x for the Laravel integration, subject to the Composer constraints

Installation

composer require unknowingpro/php-ai

The package includes Laravel support in its main Composer requirements. Laravel package discovery registers the service provider automatically; no manual provider registration is required.

Framework-agnostic PHP

The core API works without Laravel:

<?php

use PhpAi\AI;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);

echo $ai->prompt('claude-opus-5', 'Say hello in five words.');

For longer requests, use the immutable Request builder:

<?php

use PhpAi\AI;
use PhpAi\Request;

$ai = AI::openai($_ENV['OPENAI_API_KEY']);

$response = $ai->run(
    Request::to('gpt-4o-mini')
        ->system('You are concise.')
        ->user('Explain server-sent events.')
        ->maxTokens(512),
);

echo $response->text;
echo $response->usage->totalTokens();

generate() performs exactly one provider round trip. run() performs the tool loop when tools are present and otherwise behaves like a normal generation call.

Providers

Built-in entry points are available for:

  • Anthropic Messages API: AI::anthropic($apiKey)
  • OpenAI Responses API: AI::openai($apiKey)
  • OpenAI Chat Completions: AI::openaiChat($apiKey)
  • Google Gemini: AI::gemini($apiKey)

OpenAI-compatible presets are available through PhpAi\Providers\OpenAI\Compatible:

<?php

use PhpAi\AI;
use PhpAi\ProviderRegistry;
use PhpAi\Providers\OpenAI\Compatible;

$ai = AI::of(
    (new ProviderRegistry())
        ->register('ollama', Compatible::ollama())
        ->register('groq', Compatible::groq($_ENV['GROQ_API_KEY']))
        ->register('router', Compatible::openRouter($_ENV['OPENROUTER_API_KEY'])),
);

echo $ai->provider('ollama')->prompt('llama3.1', 'Say hello.');

Additional presets include DeepSeek, Mistral, xAI, Together, and arbitrary local OpenAI-compatible servers through Compatible::localServer().

Model IDs are passed through to the provider. The built-in catalogs include capability data for model families, but provider accounts and model availability remain the final authority. Use a model ID enabled for your account. The catalog includes examples such as claude-opus-5, gpt-4o-mini, gemini-2.0-flash, and text-embedding-3-small for the corresponding providers.

Streaming

Use streamText() for the common text-only case:

<?php

use PhpAi\AI;
use PhpAi\Request;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);
$stream = $ai->streamText(Request::to('claude-opus-5')->user('Write a haiku.'));

foreach ($stream as $delta) {
    echo $delta;
    flush();
}

$response = $stream->getReturn();

For typed events, use stream():

<?php

use PhpAi\AI;
use PhpAi\Request;
use PhpAi\Streaming\Events\StreamEnd;
use PhpAi\Streaming\Events\TextDelta;
use PhpAi\Streaming\Events\ToolCallComplete;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);
$request = Request::to('claude-opus-5')->user('Write a haiku.');

foreach ($ai->stream($request) as $event) {
    match (true) {
        $event instanceof TextDelta => print $event->text,
        $event instanceof ToolCallComplete => print "\\nCalling {$event->call->name}\\n",
        $event instanceof StreamEnd => print "\\n{$event->response->usage->outputTokens} output tokens\\n",
        default => null,
    };
}

Events include text, thinking, tool-call, usage, step, error, and stream-end events. A provider stream ends with StreamEnd carrying the assembled response. When using streamRun(), tool calls are executed between turns.

Streaming failover is deliberately conservative: a fallback may be selected if the primary fails before emitting an event. Once output has been emitted, the exception is propagated rather than restarting the request and duplicating output.

Tool calling

Give a tool a handler and run() executes the loop:

<?php

use PhpAi\AI;
use PhpAi\Request;
use PhpAi\Schema\Schema;
use PhpAi\Tools\Tool;

$weather = Tool::make(
    name: 'get_weather',
    description: 'Call whenever the user asks for current weather conditions in a city.',
    parameters: ['city' => Schema::string('City name')],
    handler: static fn (array $input): array => [
        'city' => $input['city'],
        'celsius' => 22,
        'sky' => 'clear',
    ],
);

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);
$response = $ai->run(
    Request::to('claude-opus-5')
        ->user('What is the weather in Paris?')
        ->tools([$weather]),
);

echo $response->text;

A throwing handler becomes an error result visible to the model. A missing handler returns pending tool calls so the caller can execute them. maxSteps() limits the number of model round trips; the default is 10.

Structured output

<?php

use PhpAi\AI;
use PhpAi\Schema\Schema;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);
$data = $ai->structured(
    'claude-opus-5',
    'Extract Jane Doe (jane@example.com) upgraded to Enterprise.',
    Schema::object([
        'name' => Schema::string('Full name'),
        'email' => Schema::string()->format('email'),
        'plan' => Schema::enum(['Free', 'Pro', 'Enterprise']),
        'notes' => Schema::string()->optional(),
    ]),
);

The library selects the strongest available strategy from the model catalog:

Strategy Description
Native Provider-native schema-constrained output when supported
Tool A synthetic tool whose input schema is the requested schema
JSON Prompted JSON with tolerant parsing as a final fallback

Use Request::structuredMode() to force a strategy. Schema properties are required by default; use optional() or nullable() as appropriate.

Conversations and media

Requests are immutable, so append the previous assistant message to continue a conversation:

<?php

use PhpAi\AI;
use PhpAi\Request;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY']);
$request = Request::to('claude-opus-5')->user('My name is Alice.');
$response = $ai->run($request);

$response = $ai->run(
    $request
        ->message($response->toMessage())
        ->user('What is my name?'),
);

Images and documents can be attached as content parts:

<?php

use PhpAi\Messages\Content\DocumentPart;
use PhpAi\Messages\Content\ImagePart;
use PhpAi\Messages\Content\TextPart;
use PhpAi\Messages\UserMessage;
use PhpAi\Request;

$request = Request::to('claude-opus-5')->message(
    UserMessage::withImages('What is in these images?', [
        'https://example.com/chart.png',
        '/local/path/photo.jpg',
        'data:image/png;base64,...',
    ]),
);

$request = Request::to('claude-opus-5')->message(
    new UserMessage([
        new TextPart('Summarise this contract.'),
        DocumentPart::fromPath('/path/contract.pdf')->withCitations(),
    ]),
);

Remote image URLs are supported by Anthropic and OpenAI. Gemini image input uses bytes; use ImagePart::fromPath() or ImagePart::fromBase64() for Gemini requests. The UserMessage::withImages() helper accepts image sources and converts them into image parts.

Embeddings

OpenAI and Gemini provide embeddings through the unified API:

<?php

use PhpAi\AI;

$ai = AI::openai($_ENV['OPENAI_API_KEY']);
$vectors = $ai->embed('text-embedding-3-small', ['hello', 'world']);

Embedding calls pass through configured middleware, including retry, failover, logging, and cache middleware. Anthropic does not provide embeddings.

Middleware

Middleware factories are applied in listed order; the first middleware is outermost. They apply consistently to generation, streaming, and embeddings:

<?php

use PhpAi\AI;
use PhpAi\Middleware\CacheMiddleware;
use PhpAi\Middleware\LoggingMiddleware;
use PhpAi\Middleware\RetryMiddleware;

$ai = AI::anthropic($_ENV['ANTHROPIC_API_KEY'])->with(
    static fn ($provider) => RetryMiddleware::wrap($provider, maxAttempts: 3),
    static fn ($provider) => CacheMiddleware::wrap($provider, $psr16Cache, ttlSeconds: 60),
    static fn ($provider) => LoggingMiddleware::wrap($provider, $psr3Logger),
);

Available middleware:

Middleware Behavior
RetryMiddleware Retries retryable API failures with exponential backoff and jitter
LoggingMiddleware Logs provider operations through PSR-3
CacheMiddleware Caches unary generation and embedding results through PSR-16
FailoverMiddleware Uses the next provider for retryable failures; never restarts a partial stream

Provider-specific fields can be passed with Request::withProviderOptions().

Laravel integration

The Laravel service provider is registered automatically through Composer package discovery.

Configuration

composer require unknowingpro/php-ai
php artisan vendor:publish --tag=php-ai-config

Configure credentials and defaults in .env:

PHP_AI_PROVIDER=openai
PHP_AI_MODEL=gpt-4o-mini
OPENAI_API_KEY=your-key
OPENAI_MODEL=gpt-4o-mini
ANTHROPIC_API_KEY=your-key
GEMINI_API_KEY=your-key
PHP_AI_RETRY_ENABLED=true
PHP_AI_CACHE_ENABLED=true

The published config/php-ai.php file supports provider keys and URLs, default provider/model, timeout, connect timeout, stream idle timeout, proxy, TLS verification, custom headers, retry settings, cache settings, failover providers, and custom middleware. Configuration is loaded with mergeConfigFrom() and works with php artisan config:cache.

Retry, cache, failover, and custom middleware are applied in the listed order, with the first configured middleware outermost. Failover providers are named entries in php-ai.providers:

'failover' => [
    'enabled' => true,
    'providers' => ['openai', 'anthropic'],
],

Custom middleware entries must be middleware class names resolvable by Laravel or closures returning a PhpAi\Contracts\Middleware. Invalid entries produce a clear configuration exception during manager construction.

Container and facade

The configured PhpAi\AI manager is bound as a singleton and is also available as php-ai. The facade resolves that same instance:

<?php

use PhpAi\Laravel\Facades\AI;
use PhpAi\Request;

$response = AI::run(
    Request::to(config('php-ai.providers.openai.model', 'gpt-4o-mini'))
        ->user('Summarise this text.'),
);

You can also inject PhpAi\AI into controllers, jobs, and commands. The package includes Laravel integration tests using Orchestra Testbench; applications can use PhpAi\Testing\FakeProvider for isolated tests.

The core package also supports manual construction through AI::anthropic(), AI::openai(), and the other static factories. Manual registration is only needed when you intentionally do not use the package's Laravel service provider.

See the dedicated Laravel guide for more examples.

Errors

All library exceptions extend PhpAi\Exceptions\AiException. API failures are classified as authentication, permission, not-found, request, rate-limit, overloaded, server, or transport exceptions. Retry only retryable failures and inspect normalized Response::$finishReason values such as Stop, ToolCalls, Length, Refusal, Pause, and Unknown.

Testing

The package includes FakeProvider, ResponseFactory, and StreamFactory for tests without network calls. The assertion helpers receive a PHPUnit TestCase instance:

<?php

use PhpAi\AI;
use PhpAi\Request;
use PhpAi\Testing\FakeProvider;
use PhpAi\Testing\ResponseFactory;

$provider = FakeProvider::make()
    ->willReturn(ResponseFactory::text('Hello'))
    ->willReturn(ResponseFactory::text('Again'));

$ai = AI::using($provider);
$response = $ai->generate(Request::to('model')->user('Hi'));

$provider->assertCalledTimes($this, 1);

The fake provider has independent generation, streaming, and embedding exception queues. It records requests and provides deterministic queued responses, stream events, and vectors.

Provider guides

Verification

The current repository verification passed with the documented Docker workflow. See CHANGELOG.md for the initial 1.0.0 release notes:

./bin/php-docker composer validate
./bin/php-docker composer install --no-interaction
./bin/php-docker vendor/bin/pint --test
./bin/php-docker vendor/bin/phpstan analyse --memory-limit=1G
./bin/php-docker vendor/bin/phpunit --no-coverage

Results:

  • Composer validation: passed
  • Composer install: passed
  • Pint: passed
  • PHPStan: passed
  • PHPUnit: passed — 385 tests, 867 assertions

Laravel 12 was exercised in the verification environment through Orchestra Testbench. The GitHub Actions matrix runs Laravel 10, 11, and 12 independently; the first release should be considered fully multi-version verified only after those CI jobs pass.

Development

The Docker helper runs the project toolchain without requiring local PHP:

./bin/php-docker composer install
./bin/php-docker vendor/bin/phpunit --no-coverage
./bin/php-docker vendor/bin/phpstan analyse --memory-limit=1G
./bin/php-docker vendor/bin/pint --test

License

This project is open-sourced under the MIT License.