papi-ai/laravel
Laravel integration for Papi AI that connects your application to AI services with minimal setup. Includes a service provider, configuration options, and a clean, expressive API for sending prompts and handling AI responses from anywhere in your Laravel app.
Prerequisites to confirm before you type anything. This is a pre-1.0 bridge, so read composer.json on the tagged release first rather than trusting the README. You need to know three things: the supported PHP range, the supported Laravel range, and the hard floor on papi-core (>= 0.15.3 as of v0.15.1). If your root composer.json pins Laravel or PHP outside those ranges, stop here — the rest of this is not your problem yet.
Install and watch the lockfile.
composer require papi-ai/laravel
git diff composer.lock
Because the package only declares a floor on papi-core, this command can move other things in the tree. Diff the lockfile and read every changed entry, not just the two named packages. If resolution fails, it is almost always papi-core conflicting with a pin elsewhere:
composer why papi-core
composer update papi-ai/laravel papi-core --with-dependencies
Publish the config and read it.
php artisan vendor:publish
Pick the PapiAI entry out of the list (don't guess the tag — the list is authoritative) and open the published file. Everything you need to know about this package is in that config file: the env var names, the base URL, timeout and retry settings, and usually the class name of the container binding. Populate .env from the key names you actually find there, not from a blog post:
PAPI_API_KEY=
PAPI_BASE_URL=
If the service provider isn't auto-discovered, register it in bootstrap/providers.php (Laravel 11+) or config/app.php. Check extra.laravel.providers in the package's composer.json — if it's there, you're done.
Where to look first, in this order: the published config → the service provider's register() method (that's where the container binding lives, and it tells you the exact class/interface to type-hint) → the package's src/ for exception classes → the papi-core package for the actual API surface.
First use case: one call, end to end. Don't build a feature first. Write a throwaway artisan command or use php artisan tinker to make a single call through the container binding, dump the response, and confirm credentials, base URL, and error handling all behave:
$client = app(\PapiAI\...\Client::class); // exact class is in the provider
$response = $client->...;
dump($response);
If that works, the plumbing is sound and everything after this is application design.
Inject, don't resolve. The package's whole value proposition is the container binding. Type-hint in constructors and let Laravel wire it — never call app() or a facade from deep inside your domain code. This matters more than usual here because it's the seam you'll use to fake the client in tests and to survive the next breaking change.
Wrap it in your own gateway class. At 0.x, this package's public API is not stable. Isolate it behind one class you own, so a minor bump that renames a method becomes a one-file change instead of a grep across the codebase:
final class AiSummaryGateway
{
public function __construct(private readonly Client $client) {}
public function summarize(string $text): string
{
try {
return $this->client->...->text();
} catch (PapiException $e) { // confirm the real exception class
throw new SummaryUnavailable(previous: $e);
}
}
}
Bind that gateway to an interface in a service provider, and have the rest of the app depend on your interface. The package then appears in exactly two places: the provider and the gateway.
Push long calls onto the queue. AI endpoints are slow and flaky in ways normal HTTP endpoints aren't. Anything user-facing should dispatch a job, with an explicit timeout and a retry policy that reflects the cost and non-idempotency of generation calls:
public int $tries = 2;
public int $backoff = 30;
public int $timeout = 120;
Set the timeout explicitly in config rather than relying on the HTTP client default — Laravel's default is far too short for generation endpoints.
Test with Http::fake(), not by mocking the client. The package advertises "HTTP client integration following Laravel conventions," which means it rides on the framework's HTTP client. That gives you the whole Http::fake() / Http::assertSent() toolkit, and it exercises the package's real request-building and error-mapping code rather than a stub:
Http::fake(['api.papi.example/*' => Http::response(['...' => '...'], 200)]);
Use the container swap ($this->app->instance(Interface::class, $fake)) only for tests of code that depends on your gateway interface.
Handle errors at one boundary. The package advertises centralized error handling — find the exception type in src/ and catch it in exactly one place per feature (the gateway, or a global handler). Don't scatter try/catch through controllers; you'll end up with three different user-facing messages for the same upstream 429.
Version-pin deliberately and co-bump. Do not float the constraint. Pin papi-ai/laravel and papi-core to explicit ranges, and write the upgrade as a single PR that bumps both, with papi-core landing first. Their version numbers do not track each other — v0.15.1 of this package corresponds to papi-core 0.15.3+, and that skew will confuse your dependency bot if you let it open the PRs independently.
The floor is hard, not advisory. v0.15.1 is a constraint-only release: it exists to force papi-core >= 0.15.3, which is where the PHP 8.5 fixes live. If you're mid-PHP-8.5-migration, this package is the green light, not the fix — sequence it after the core bump and let it soak before the runtime moves.
Pre-1.0 means a minor bump can break you. At 0.x, 0.15 → 0.16 may legitimately rename or remove methods. Read the changelog for every release, and treat the absence of a changelog between versions as a reason to diff src/ yourself.
Config caching bites in production. php artisan config:cache freezes env values. If you add PAPI_API_KEY to .env on a cached deployment and it "doesn't work," re-run config:cache (or config:clear) before you debug anything else. This is the single most common false alarm with publishable-config packages.
Retries on generation endpoints can double your bill and your side effects. A retried request that actually succeeded server-side is a duplicate. Only enable aggressive retries for read-style calls; for generation, prefer a single retry with backoff plus an idempotency key if the config exposes one.
The container binding is your extension point. If the shipped client doesn't fit (custom middleware for tracing, a different transport, a proxy), rebind the interface in your own service provider after the package's provider. That's the sanctioned seam and it's cheaper than forking.
Debugging request/response traffic: register a global HTTP client middleware or listen on the framework's HTTP events (Illuminate\Http\Client\Events\*) so you can log payloads without touching package code. Never dump raw request payloads to logs in production without redaction — they contain user content.
Don't assume docs exist. This package has 0 dependents and 1 star. The published config plus composer.json plus src/ are the real documentation; budget an hour to read them before planning work, and verify the class and exception names used in any snippet above against the source. The shapes here are conventional for a Laravel bridge, but they are inferred from the description, not from the code.
No support policy is stated anywhere. Before this reaches a production path, check the repository's release cadence and issue-response history. That's a bigger risk than any technical detail on this page — and if papi-core isn't already in your stack, the honest question is whether this wrapper justifies adopting it.
How can I help you explore Laravel packages today?