openai-php/client
OpenAI PHP is a community-maintained PHP client for the OpenAI API, giving Laravel and PHP applications typed, framework-agnostic access to chat, responses, embeddings, images, audio, files, and streaming, backed by an active community and thousands of GitHub stars.
Updated for openai-php/client v0.21.0. This release adds Responses API support for programmatic tool calling and the apply_patch tool, improves streamed response processing, allows nullable file-search options, exposes cache_write_tokens, and supports web search image results/settings. No breaking changes are listed in the release notes, but agent/tool and streaming code paths should be re-tested.
Install the package:
composer require openai-php/client
If your app does not already have a PSR-18 HTTP client, add Guzzle:
composer require guzzlehttp/guzzle
Configure the client in config/openai.php:
return [
'api_key' => env('OPENAI_API_KEY'),
'base_uri' => env('OPENAI_BASE_URI', 'https://api.openai.com/v1'),
'default_model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
'timeout' => (int) env('OPENAI_TIMEOUT', 30),
];
First call using the Responses API, which is the modern default for chat-style work:
use OpenAI\Client;
$client = OpenAI::client(config('openai.api_key'));
$response = $client->responses()->create([
'model' => config('openai.default_model'),
'input' => 'Summarize the following support ticket in two sentences: ...',
]);
return $response->outputText;
Where to look first:
responses() for most day-to-day text generation, chat, tools, and multimodal flows.models() for model availability checks.files() and related resources for upload-backed workflows.Common first use case: wrap responses()->create() in a Laravel service and use it for support summaries, drafting, classification, or retrieval-augmented answers.
Bind a configured client once so services do not read env() at runtime:
use OpenAI\Client;
public function register(): void
{
$this->app->singleton(Client::class, function () {
return OpenAI::factory()
->withApiKey(config('openai.api_key'))
->withBaseUri(config('openai.base_uri'))
->withHttpClient(new \GuzzleHttp\Client([
'timeout' => config('openai.timeout'),
]))
->make();
});
}
Service-layer wrapper:
class OpenAIService
{
public function __construct(private Client $client) {}
public function answer(string $input): string
{
$response = $this->client->responses()->create([
'model' => config('openai.default_model'),
'input' => $input,
'temperature' => 0.7,
]);
return $response->outputText;
}
}
Function tools and follow-ups: define normal function tools in the tools array, execute the returned function call in your app, then send the tool result back to the model. Keep a maximum tool-call/agent-turn budget to avoid runaway loops.
$response = $this->client->responses()->create([
'model' => config('openai.default_model'),
'input' => 'What is the weather in Barcelona?',
'tools' => [[
'type' => 'function',
'name' => 'fetch_weather',
'description' => 'Fetch current weather for a city.',
'parameters' => [
'type' => 'object',
'properties' => [
'city' => ['type' => 'string'],
],
'required' => ['city'],
],
]],
]);
foreach ($response->output as $item) {
if (($item->type ?? null) === 'function_call') {
$args = json_decode($item->arguments, true);
$weather = $this->fetchWeather($args['city']);
// Send function_call_output back in a follow-up Responses request.
}
}
Programmatic tool calling (v0.21.0): use this when you want the model to orchestrate tool use within the Responses flow rather than driving every step manually in PHP. Treat it as an agent workflow: validate every tool input, authorize tool execution against the current user, cap turns, and log each tool call. Start with a small allow-list of tools instead of exposing all application actions.
Apply-patch tool (v0.21.0): useful for code-editing/agent workflows, but do not apply model-generated patches directly to production files. Run patches in a sandbox or temporary branch, validate the diff, run tests, and require review before merging.
File search with optional nulls (v0.21.0): ranking_options and max_num_results may now be null in the FileSearchTool. If your config builder uses optional values, avoid forcing empty arrays or placeholder values:
'tools' => [[
'type' => 'file_search',
'vector_store_ids' => [$vectorStoreId],
'max_num_results' => $maxResults ?: null,
'ranking_options' => $rankingOptions ?: null,
]],
Web search image results/settings (v0.21.0): web search responses may now contain image content and additional settings. If you render results in Blade/Inertia/Livewire, handle non-text content blocks instead of assuming every output item is plain text.
Streaming: createStreamed() is still the right choice for chat UIs. v0.21.0 includes streamed-response processing optimizations, but UI code should still treat chunk boundaries as arbitrary:
$stream = $client->responses()->createStreamed([
'model' => config('openai.default_model'),
'input' => 'Explain Laravel middleware briefly.',
]);
$buffer = '';
foreach ($stream as $chunk) {
$buffer .= $chunk->delta->content[0]->text ?? '';
if (preg_match('/[.!?]\s*$/', $buffer)) {
echo $buffer;
flush();
$buffer = '';
}
}
Usage and cost logging: account for the new cache_write_tokens field in prompt token details. It affects cost estimates when prompt caching is used:
$usage = $response->usage;
Log::info('OpenAI usage', [
'input_tokens' => $usage->inputTokens ?? null,
'output_tokens' => $usage->outputTokens ?? null,
'cache_write_tokens' => $usage->inputTokensDetails->cacheWriteTokens ?? null,
]);
Multi-turn conversations: use previous_response_id to continue from a prior response, or persist your own message/tool-call history. Store the response ID alongside the Laravel conversation/session record.
Error handling: centralize OpenAI/transport failures in Laravel’s exception handler or a dedicated service. Return safe API errors to clients, log request context, and do not leak prompts or API keys.
apply_patch output directly. Treat patches as untrusted code changes. Apply only after validation, tests, and review.config() inside services. env() should live in config files; run php artisan config:cache in deployment and clear config after changing values.ranking_options and max_num_results can be null now; only send values your application actually needs.cache_write_tokens. Existing token/cost dashboards may undercount prompt-cache writes until updated.How can I help you explore Laravel packages today?