DEV Community

Cover image for Aimeos Prisma 0.5: a sturdier PHP layer for AI providers
Aimeos
Aimeos

Posted on

Aimeos Prisma 0.5: a sturdier PHP layer for AI providers

Most AI integrations do not fail because the first provider is hard to call. They become awkward later, when the application has to speak several dialects at once.

One endpoint generates text. Another returns embeddings. A third handles transcription. Image generation arrives with its own payload shape, its own errors, and its own idea of metadata. Add web search, tool calling, local models, or an OpenAI-compatible gateway, and the useful application code starts to disappear behind provider plumbing.

Aimeos Prisma exists to keep that boundary small. It gives PHP applications one API across text, image, audio, and video providers, while still leaving room for provider-specific options when they are needed.

The 0.5 release makes that layer more suitable for real applications: streamed responses, conversation history, stronger schema support, request observation, better test doubles, safer response handling, and more provider coverage.

composer require aimeos/prisma
Enter fullscreen mode Exit fullscreen mode

The API shape

Prisma is a framework-agnostic PHP 8.2+ package. It is MIT-licensed, uses Guzzle internally, and keeps the calling pattern consistent across provider types.

use Aimeos\Prisma\Prisma;

$text = Prisma::text()
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->write('Summarize the benefits of renewable energy')
    ->text();

$image = Prisma::image()
    ->using('stabilityai', ['api_key' => getenv('STABILITYAI_API_KEY')])
    ->imagine('a clean product photo of a ceramic coffee cup')
    ->binary();

$transcript = Prisma::audio()
    ->using('deepgram', ['api_key' => getenv('DEEPGRAM_API_KEY')])
    ->transcribe($audioFile)
    ->text();

$description = Prisma::video()
    ->using('gemini', ['api_key' => getenv('GEMINI_API_KEY')])
    ->describe($videoFile)
    ->text();
Enter fullscreen mode Exit fullscreen mode

The provider name changes. The surrounding application shape does not.

Streaming belongs in the core API

For user-facing tools, waiting for a full model response is often the wrong interaction. Prisma 0.5 adds stream() as a text capability instead of treating it as a separate integration path.

use Aimeos\Prisma\Prisma;

$response = Prisma::text()
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->ensure('stream')
    ->stream('Write a short launch announcement for our PHP package');

foreach ($response->stream() as $delta) {
    echo $delta;
}

$full = $response->output();
$usage = $response->usage();
Enter fullscreen mode Exit fullscreen mode

Streaming keeps the same configuration surface as write(): model selection, system prompts, tools, prior messages, and provider options all travel through the same provider object.

In Laravel, it can be handed directly to response()->eventStream():

use Aimeos\Prisma\Prisma;
use Illuminate\Support\Facades\Route;

Route::get('/chat', function () {
    $response = Prisma::text()
        ->using('openai', config('services.openai'))
        ->ensure('stream')
        ->stream('Explain event sourcing in plain English');

    return response()->eventStream(function () use ($response) {
        foreach ($response->stream() as $chunk) {
            if (is_string($chunk)) {
                yield $chunk;
            }
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

There is one operational rule: consume the stream before reading body-derived metadata. Usage, citations, tool steps, finish reason, and response metadata are complete only after the stream has been drained. Rate-limit data from headers is available immediately.

Conversation history without a second abstraction

Chat history now lives in withMessages(). The current prompt passed to write(), stream(), or structure() becomes the next user turn.

use Aimeos\Prisma\Prisma;

$response = Prisma::text()
    ->using('anthropic', ['api_key' => getenv('ANTHROPIC_API_KEY')])
    ->withSystemPrompt('You are a concise technical assistant.')
    ->withMessages([
        ['role' => 'user', 'content' => 'I need a laptop recommendation.'],
        ['role' => 'assistant', 'content' => 'What is your budget and workload?'],
    ])
    ->write('Around 1500 EUR, mostly PHP development and light video editing.');

echo $response->text();
Enter fullscreen mode Exit fullscreen mode

For multimodal providers, user turns can also carry files, subject to each provider's file support.

Structured output is stricter when it can be, explicit when it cannot

Structured output is most useful when the code can state what it expects and then check what it received. Prisma 0.5 improves both sides.

use Aimeos\Prisma\Prisma;
use Aimeos\Prisma\Schema\Schema;

$schema = Schema::for('ticket', [
    'title' => Schema::string()->required(),
    'priority' => Schema::string()->enum(['low', 'normal', 'high'])->required(),
    'tags' => Schema::array()->items(Schema::string()),
]);

$response = Prisma::text()
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->ensure('structure')
    ->structure('Extract a support ticket from: Checkout is broken for EU cards', $schema);

$ticket = $response->structured();
Enter fullscreen mode Exit fullscreen mode

By default, Prisma uses the provider's native structured-output mode when one exists. If a schema is too large or deeply nested for that mode, pass ['mode' => 'json'] and Prisma will embed the schema in the prompt and parse the returned JSON.

$response = Prisma::text()
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->structure('Extract a support ticket', $schema, [], ['mode' => 'json']);
Enter fullscreen mode Exit fullscreen mode

The schema builder also gained unions, reusable definitions, references, and validation:

use Aimeos\Prisma\Schema\Schema;

$schema = Schema::for('order', [
    'billing' => Schema::ref('Address')->required(),
    'shipping' => Schema::ref('Address')->required(),
    'discount' => Schema::anyOf([
        Schema::string(),
        Schema::number(),
    ]),
])->def('Address', Schema::object([
    'street' => Schema::string()->required(),
    'city' => Schema::string()->required(),
]));

$errors = $schema->validate($response->structured());

if ($errors) {
    // reject, retry, or ask for a correction
}
Enter fullscreen mode Exit fullscreen mode

Provider-enforced structured output reduces risk, but it does not make generated content trusted. Validate the shape you receive, and handle generated values as carefully as any user-supplied input.

Wider provider coverage

Prisma 0.5 now covers 29 providers across text, image, audio, and video workflows. The release adds Azure OpenAI text support, VertexAI text support, image generation through xAI, ModelsLab, and Replicate, and text embeddings across more providers.

OpenAI-compatible endpoints remain a practical escape hatch. Local servers, internal gateways, and proxy services can be used through the openai provider by changing the base URL:

$response = Prisma::text()
    ->using('openai', [
        'api_key' => getenv('GATEWAY_API_KEY'),
        'url' => 'https://my-gateway.example.com',
    ])
    ->model('my-model')
    ->write('Hello from an OpenAI-compatible gateway');
Enter fullscreen mode Exit fullscreen mode

Provider tools also behave more carefully in 0.5. Prisma can omit unsupported combinations, such as a server-side tool that a provider cannot use for the current request shape, without forcing every application to know those edge cases.

Observability where the call happens

Production AI calls need accounting. A plain response string cannot tell you which model ran, how long the request took, whether it failed, or what usage the provider reported.

Prisma 0.5 adds request-scoped observers:

use Aimeos\Prisma\Prisma;
use Aimeos\Prisma\Values\Observation;

$response = Prisma::text()
    ->observe(function (Observation $observation) {
        logger()->info('ai.operation', $observation->toArray());
    })
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->model('gpt-4.1-mini')
    ->write('Draft a changelog entry');
Enter fullscreen mode Exit fullscreen mode

An observation records the operation, provider type, provider name, model, duration, error state, usage, and metadata. Streamed responses are observed after the stream completes, so their usage and metadata are present.

Usage and metadata now have names

Provider usage payloads are inconsistent. input_tokens, prompt_tokens, promptTokenCount, and inputTokens may all mean the same thing, depending on which API answered.

In 0.5, usage() returns a Values\Usage object:

$usage = $response->usage();

$usage->promptTokens();
$usage->completionTokens();
$usage->totalTokens();
$usage->cacheReadTokens();
$usage->cacheWriteTokens();
$usage->thoughtTokens();
$usage->used();
Enter fullscreen mode Exit fullscreen mode

meta() returns a Values\Meta object:

$meta = $response->meta();

$meta->id();
$meta->model();
$meta->thinking();
$meta->reasoningDetails();
Enter fullscreen mode Exit fullscreen mode

Both objects remain array-compatible for provider-specific fields:

$rawUsage = $response->usage()->all();
$created = $response->meta()['created'] ?? null;
Enter fullscreen mode Exit fullscreen mode

Existing code that type-hints these results as arrays should switch to Values\Usage, Values\Meta, or call all() when a raw array is required.

Testing without a live provider

Application tests should not need live API keys to prove that the right Prisma calls are made. Prisma 0.5 adds a recording fake:

use Aimeos\Prisma\Prisma;
use Aimeos\Prisma\Responses\TextResponse;

$fake = Prisma::fake([
    TextResponse::fake('Invoice approved')
        ->withUsage(12, ['prompt_tokens' => 5, 'completion_tokens' => 7]),
]);

$result = Prisma::text()
    ->using('openai', ['api_key' => 'test'])
    ->write('Review this invoice')
    ->text();

$fake->assertCalled('write', function (array $args) {
    return str_contains($args[0], 'invoice');
});

Prisma::reset();
Enter fullscreen mode Exit fullscreen mode

For provider-level tests, the new testing helpers can run real provider code against mocked Guzzle responses. Request building, response parsing, streaming, and tool loops can be exercised without leaving the test process.

Responses implement JsonSerializable, which keeps snapshot-style assertions straightforward:

$payload = $response->jsonSerialize();
Enter fullscreen mode Exit fullscreen mode

Safer response boundaries

Provider responses may be streamed, proxied, malformed, or simply too large. withMaxResponseSize() bounds how many bytes Prisma will read from a single provider response:

$response = Prisma::text()
    ->using('openai', ['api_key' => getenv('OPENAI_API_KEY')])
    ->withMaxResponseSize(16 * 1024 * 1024)
    ->write('Analyze this document');
Enter fullscreen mode Exit fullscreen mode

The default limit is 64 MB per provider response. It applies to streamed and non-streamed calls, including individual tool-loop turns.

The release also tightens provider discovery by validating provider type and name before building provider class names.

Custom providers are part of the story

Prisma resolves providers by namespace convention:

Prisma::text()->using('myprovider', $config);
Enter fullscreen mode Exit fullscreen mode

That maps to:

Aimeos\Prisma\Providers\Text\Myprovider
Enter fullscreen mode Exit fullscreen mode

The new custom provider guide documents provider discovery, contracts, response objects, structured output, tool loops, async operations, OpenAI-compatible APIs, and testing.

That is useful when a project has an internal gateway, a private model host, or a provider that Prisma does not ship yet.

Upgrading

The main migration points are small but worth checking. Use stream() for live text responses. Pass prior turns through withMessages(). Call structure() to request structured output; keep using structured() to read the parsed response. Treat usage() and meta() as value objects, or call all() when existing code expects arrays. Drain streamed responses before reading usage, metadata, citations, tool steps, or finish reason. Reset Prisma::fake() in test teardown.

Links

Docs: https://php-prisma.org

GitHub: https://github.com/aimeos/prisma

Custom providers: CUSTOM-PROVIDERS.md

Prisma 0.4 established the broad interface across text, image, audio, and video. Prisma 0.5 makes that interface easier to run in production: streamed UIs, observable calls, cheaper tests, clearer schemas, provider fallbacks, and custom integrations.

Top comments (0)