Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Laravel Package

openai-php/laravel

Community-maintained Laravel integration for the OpenAI API. Install via Composer, run php artisan openai:install to publish config, then use the OpenAI facade to call models, responses, and other endpoints.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a thin Laravel integration around openai-php/client: it gives you a deferred service provider, a config file, an OpenAI facade, and first-class test fakes. Almost everything you actually do (resource methods, response DTOs, exceptions) lives in the underlying client, so budget reading time for both repos.

Install and scaffold:

composer require openai-php/laravel
php artisan openai:install

openai:install publishes config/openai.php and appends blank OPENAI_API_KEY / OPENAI_ORGANIZATION entries to .env. Fill them in, then make one call to confirm the wiring:

use OpenAI\Laravel\Facades\OpenAI;

$response = OpenAI::responses()->create([
    'model' => 'gpt-5',
    'input' => 'Summarize this support ticket: ...',
]);

echo $response->outputText;

Where to look first, in order:

  1. config/openai.php — confirms exactly which env vars exist (OPENAI_API_KEY, OPENAI_ORGANIZATION, OPENAI_PROJECT, OPENAI_BASE_URL, OPENAI_REQUEST_TIMEOUT).
  2. vendor/openai-php/client/src/Resources/ — the authoritative list of resources and method signatures. The Laravel README explicitly defers to the client repo for usage examples; don't expect to find assistants(), batch(), embeddings() docs here.
  3. vendor/openai-php/client/src/Responses/ — typed DTOs, which is how you discover response fields without guessing from JSON.

A good first real use case: a queued job that enriches a model record. It exercises the facade, the timeout config, a retry path on rate limits, and a fake in the test — the four things that actually bite in production.

final class SummarizeTicket implements ShouldQueue
{
    public int $tries = 3;
    public array $backoff = [10, 30, 60];

    public function __construct(public int $ticketId) {}

    public function handle(): void
    {
        $ticket = Ticket::findOrFail($this->ticketId);

        $response = OpenAI::responses()->create([
            'model' => 'gpt-5',
            'input' => $ticket->body,
            'max_output_tokens' => 300,
        ]);

        $ticket->update(['summary' => $response->outputText]);
    }
}

Implementation Patterns

Inject the client, façade inside the service layer only. The facade is convenient but static, which makes unit testing and multi-tenant key swapping awkward. The provider binds OpenAI\Client as a singleton, so type-hint the interface in constructors and reserve OpenAI:: for controllers, jobs, and Artisan commands.

namespace App\Services\Ai;

use OpenAI\Client;

final class TicketSummarizer
{
    public function __construct(private readonly Client $openai) {}

    public function summarize(string $body): string
    {
        return $this->openai->responses()->create([
            'model' => 'gpt-5',
            'input' => $body,
        ])->outputText;
    }
}

Wrap every call in an application-level abstraction. TicketSummarizer, EmbeddingIndexer, ModerationGate — one class per AI capability. This is the single highest-leverage decision: it localises model name changes, prompt tweaks, token accounting, and exception translation, and it means swapping providers later is a container binding rather than a grep across 40 files.

Everything slow goes on a queue. A chat completion is a network round-trip measured in seconds; a reasoning-model call can be tens of seconds. Never do it inside a web request unless the UI is explicitly waiting on it. Configure $tries and $backoff on the job, and catch RateLimitException specifically:

use OpenAI\Exceptions\RateLimitException;
use OpenAI\Exceptions\ErrorException;

try {
    $response = OpenAI::responses()->create([...]);
} catch (RateLimitException $e) {
    $this->release(60); // let the queue back off rather than sleeping
} catch (ErrorException $e) {
    report($e);
    throw $e;
}

Use OpenAI::fake() religiously; never let CI touch the network. Fakes are consumed in the order supplied, so seed them per test case. Response classes each expose a static fake() so you don't hand-build nested arrays.

use OpenAI\Laravel\Facades\OpenAI;
use OpenAI\Responses\Responses\CreateResponse;
use OpenAI\Resources\Responses;

OpenAI::fake([
    CreateResponse::fake(['output_text' => 'Refund approved.']),
]);

$this->post('/tickets/1/summarize')->assertOk();

OpenAI::assertSent(Responses::class, fn (string $method, array $params): bool =>
    $method === 'create' && $params['model'] === 'gpt-5'
);

Prefer the responses() resource over chat()/completions() for new work. completions(), edits(), and fineTunes() are marked deprecated in the client; responses(), chat(), embeddings(), fineTuning(), and the newer conversations(), containers(), and realtime() resources are where the API is heading. Mixing generations in one codebase is the main source of "why does field X not exist here" confusion.

Structured output beats string parsing. Request JSON schema output and validate with a FormRequest or a Validator before writing to the database — treat model output as untrusted user input, because it is:

$response = OpenAI::responses()->create([
    'model' => 'gpt-5',
    'input' => $prompt,
    'text' => ['format' => ['type' => 'json_schema', 'name' => 'ticket', 'schema' => $schema]],
]);

$data = json_decode($response->outputText, true);
Validator::make($data, ['priority' => 'required|in:low,high'])->validate();

Route through OPENAI_BASE_URL in local and staging. Pointing staging at a stub or gateway (rather than the real endpoint) keeps prod keys out of developer laptops and lets you record/replay traffic. It costs one env var.

Track usage next to the call, not in a dashboard later. Every response carries usage metadata; log it with the user/tenant ID in the same place you make the request, or cost attribution becomes archaeology:

Log::info('openai.call', [
    'tenant' => tenant()->id,
    'model' => 'gpt-5',
    'input_tokens' => $response->usage->inputTokens,
    'output_tokens' => $response->usage->outputTokens,
]);

Per-tenant keys are not a config concern. Build a client from the client package's factory and bind it per request (e.g. in a scoped container binding or a TenantClientFactory), rather than mutating config() at runtime.

Gotchas and Tips

Http::fake() does not intercept this package. The client uses its own PSR-18 transporter, not Laravel's HTTP client, so Http::preventStrayRequests() and Http::fake() give you zero protection — a test can silently make a real, billable API call. Use OpenAI::fake() and, defensively, throw when OPENAI_API_KEY is unset in the testing env.

Fake responses are a queue, not a matcher. The Nth call gets the Nth fake. If your code path can make an extra call (a retry, a moderation pre-check), the ordering shifts and your assertions pass or fail for the wrong reason. Assert with assertSentCount() alongside assertSent().

The README's assertion example is inconsistent with its own create example. The create call uses input, the assertSent closure checks $parameters['prompt']. That's a copy-paste from the legacy completions API and will silently fail assertions. Always match on the key you actually sent.

The default timeout is 30 seconds and it is not a suggestion. Reasoning models, large max_output_tokens, and big embedding batches routinely exceed it. Raise OPENAI_REQUEST_TIMEOUT, and remember two other ceilings stack on top: PHP's max_execution_time for web requests and your queue worker's --timeout. Set all three coherently, with the worker timeout greater than the HTTP timeout, or you get jobs killed mid-request and retried — double billing.

The provider is deferred, so misconfiguration surfaces late. A missing or blank API key won't error at artisan boot; it throws on the first API call, potentially in a queue worker at 3am. Add a startup check in your deploy health endpoint or a ping-style command.

config:cache is your friend — for the config file, not for env(). All the OpenAI settings are env-backed inside config/openai.php, so they cache correctly. Just never call env('OPENAI_API_KEY') directly in application code; it returns null under a cached config.

Exception types matter for retry decisions. RateLimitException (429) is retryable with backoff; ErrorException covers 4xx/5xx API errors and is often not retryable (bad request, context length exceeded); transporter-level failures surface separately. A blanket catch (\Throwable) with $this->release() will happily burn money retrying a malformed prompt.

OPENAI_ORGANIZATION and OPENAI_PROJECT are not optional in some orgs. If you see 401s with a key you know is valid, it's usually a project-scoped key that needs OPENAI_PROJECT=proj_... set.

Response objects are immutable typed DTOs. ->outputText and friends are convenience accessors; use ->toArray() when you need to persist the raw payload for auditing or replay. ->meta() exposes headers/request IDs, which is the fastest way to get something useful into a support ticket with OpenAI.

Streaming needs a different call shape. createStreamed() returns a generator of chunks; you're responsible for flushing an SSE response and for X-Accel-Buffering: no if nginx sits in front. There is no broadcast/Echo glue — the realtime facade is likewise a raw client, not a Laravel-native WebSocket stack.

Facade docblocks lag the client. After bumping openai-php/laravel, IDE autocompletion on OpenAI:: may not reflect new resources. Read changelog/ in the package — the integration version tracks a client version, and feature availability moves in lockstep (conversations appeared in 0.17.0, fineTuning in 0.12.0, containers in 0.15.0, and 0.17.1 removed a hard-coded OpenAI-Beta header that broke the Conversations API). Pin or bump deliberately.

Treat prompts and API keys as production assets. Keys belong in a secrets store, not .env on a shared box; prompts belong in version-controlled config or dedicated classes, not concatenated inline strings. And log the request ID, not the prompt body, when the payload contains PII — this package will happily put customer text into your exception handler's context.

Cap output aggressively. max_output_tokens is the cheapest guardrail you have; without it, a runaway prompt can turn a $0.01 request into a $0.50 one, and a retry loop multiplies it. Pair it with a per-user rate limit on whatever endpoint triggers the AI call — the OpenAI rate limit is your ceiling, not your policy.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console