What does a production-ready OpenAI or Claude call in PHP need?

A production-ready LLM call in PHP needs more than the ten lines that send a prompt. It needs a single service class that owns the call, an explicit timeout for that feature, retries that actually fire on the errors these APIs return, streaming when a person is waiting, a stable prompt prefix so caching works, and a log line with model, tokens and cost for every request.

This checklist is framework-agnostic: it applies to Symfony, Laravel, Slim or plain PHP. It is written for PHP developers who own an AI feature and for CTOs who review the plan. If you work in a specific framework, our guides on adding OpenAI or Claude to Laravel and adding AI to a Symfony app go deeper into queues, controllers and tests there.

Every version number and default below was checked on 29 October 2026 against Packagist, the providers' documentation and the libraries' source code.

The checklist in short:

  1. Pick the client on purpose: official SDK, community SDK or your own HTTP client.
  2. Put every provider call behind one class and keep keys and model names in config.
  3. Set a timeout per feature, and make the web stack allow it.
  4. Retry 429, 5xx, 529 and connection errors, with backoff that honors Retry-After.
  5. Stream answers that a person waits for, with output buffering off.
  6. Ask for structured output instead of parsing free text.
  7. Keep the prompt prefix stable so provider caching applies.
  8. Send work nobody waits for through the Batch APIs.
  9. Log model, tokens, latency and cost for every call.
  10. Treat model output as untrusted input.

Where this checklist comes from

Our own project board, the one we run client work on, calls the OpenAI API from a Symfony back end for five features: it transcribes voice notes with Whisper, turns dictated text into a formatted task description, writes a business-analyst draft from a raw client request, and generates a spoken morning briefing and a daily report. It uses Symfony HttpClient directly, without an SDK, with gpt-4o-mini for text.

Writing this guide, we reviewed that client against the checklist. Some items it gets right, and we show them below. One it does not: it has no retry layer yet, so a single 429 or 503 surfaces as an error to the user. We point out where, because the gap is typical of internal tools that grew from a prototype.

Before that, we built AI Resume Master, an AI SaaS on PHP and the OpenAI API, which reached 50,000 monthly active users.

Which PHP client should you use for OpenAI and Claude?

Use the official Anthropic SDK for Claude-heavy work, the openai-php/client package for OpenAI-heavy work, and your own HTTP client when you have one or two simple features and want no extra dependency. If you need several providers behind one API, see our comparison of Laravel AI SDK, Prism, Symfony AI and LLPhant.

Client Maintainer Latest version (checked 29.10.2026) PHP Installs per month Retries built in
anthropic-ai/sdk Anthropic (official) v1.3.0, 23 October 2026 8.1+ about 1.4 million Yes: 2 retries by default
openai-php/client Community (Nuno Maduro and contributors) v0.21.0, 17 September 2026 8.2+ about 2.9 million No
openai-php/laravel / openai-php/symfony Community v0.21.0 / v0.19.0 8.2+ about 1.1 million / 91,000 No (same client)
Your own HTTP client (Symfony HttpClient, Guzzle, Laravel Http) You - - - Only what you add

OpenAI does not publish an official PHP SDK. openai-php/client is still pre-1.0, so pin the minor version in composer.json and read the changelog before upgrading.

Both SDKs send requests through a PSR-18 HTTP client that they discover automatically. That matters for the next two sections: timeouts and, for openai-php, retries are decided by that HTTP client, not by the SDK.

How do you set timeouts for LLM calls in PHP?

Give every feature its own timeout on the HTTP client, sized to the slowest normal answer for that feature, and make sure PHP-FPM, the web server and any load balancer allow at least as long.

Our board does the first part. Each method passes its own timeout to Symfony HttpClient: 30 seconds for the short spoken briefing and daily report, 60 for transcription and formatting, 90 for the business-analyst draft, which writes the longest text.

PHP
$response = $this->http->request('POST', 'https://api.openai.com/v1/chat/completions', [
    'headers' => ['Authorization' => 'Bearer ' . $this->apiKey],
    'json' => $payload,
    'timeout' => 90, // idle timeout for this feature
    'max_duration' => 120, // hard cap on the whole request
]);

In Symfony HttpClient, timeout is the maximum idle time between bytes, and max_duration is the cap on the whole request. For LLM calls you want both: a slow model that keeps sending tokens should not be killed, and a request that hangs forever should be.

Two traps we checked in the source code:

  • The Anthropic SDK's timeout option does not enforce anything by itself. The SDK documents an intended default of 600 seconds, but a comment in its RequestOptions class says plainly that the timeout is enforced by the HTTP client you pass in, not by the SDK. If you rely on auto-discovery, you get whatever that client's default is. Pass a configured client instead:
PHP
use Anthropic\Client;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;

$http = new Psr18Client(HttpClient::create(['timeout' => 60, 'max_duration' => 120]));

$claude = new Client(requestOptions: ['transporter' => $http]);
  • The rest of the stack has its own limits. PHP-FPM's request_terminate_timeout, nginx's fastcgi_read_timeout (60 seconds by default) and a load balancer's idle timeout all apply. If any of them is shorter than your API timeout, the user gets a 502 or 504 while PHP is still waiting for the model. For anything that can take longer than about a minute, move the call into a queued job and show progress, or stream.

How do you retry OpenAI and Claude API errors from PHP?

Retry on 429 (rate limit), 5xx (provider errors), Anthropic's 529 (overloaded) and connection errors, with exponential backoff that honors the Retry-After header, and stop after two or three attempts. Do not retry 400, 401 or 404: the same request will fail the same way.

What each option does out of the box:

Layer What it retries by default Backoff Honors Retry-After
Anthropic PHP SDK 408, 409, 429, every 5xx (including 529) and transport failures, up to 2 retries 0.5 s initial, 8 s cap Yes
openai-php/client Nothing - -
Symfony RetryableHttpClient (default strategy) 423, 425, 429, 502, 503 for any method; 500, 504, 507, 510 and transport errors only for idempotent methods 1 s, multiplier 2, up to 3 retries Yes
Your own code Whatever you write - -

The Symfony row is the one that surprises people. Every LLM call is a POST, and POST is not idempotent, so with the default strategy a 500, a 504 or a dropped connection is not retried at all, and 529 is not on the list either. We confirmed this in GenericRetryStrategy (symfony/http-client 7.3). To retry LLM calls with Symfony HttpClient, pass your own status code map:

PHP
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\RetryableHttpClient;

$retryOn = [0, 408, 429, 500, 502, 503, 504, 529]; // 0 = transport errors, any method
$http = new RetryableHttpClient(
    HttpClient::create(),
    new GenericRetryStrategy($retryOn, delayMs: 1000, multiplier: 2.0, maxDelayMs: 10000),
    maxRetries: 3,
);

Retrying a POST is safe here because a chat or messages call creates nothing on the provider's side; the worst case is paying for one extra generation. That is not true for every endpoint: do not blindly retry calls that create files, batches or fine-tuning jobs.

Two more rules from experience:

  • Keep the total retry time below the request timeout of whoever waits for the answer. Three retries with a 60-second timeout each can hold a web request for four minutes.
  • Log every retry with the status code. A slow rise in 429s is the first sign you need a higher rate-limit tier or a cheaper model for part of the traffic.

Both providers return rate-limit headers you can watch before you hit the wall. openai-php/client exposes them through $response->meta(), for example requestLimit->remaining and tokenLimit->remaining. Anthropic returns retry-after and anthropic-ratelimit-* headers.

How do you stream OpenAI or Claude responses from PHP?

Use the SDK's streaming method or read the provider's server-sent events yourself, write each text chunk to the response and flush it, with output buffering turned off in PHP and in the web server.

With openai-php/client and the Responses API:

PHP
$stream = $client->responses()->createStreamed([
    'model' => $model,
    'input' => $question,
]);

foreach ($stream as $event) {
    if ($event->event === 'response.output_text.delta') {
        echo 'data: ' . json_encode(['text' => $event->response->delta]) . "\n\n";
        flush();
    }
}

With the Anthropic SDK:

PHP
use Anthropic\Messages\RawContentBlockDeltaEvent;
use Anthropic\Messages\TextDelta;

$stream = $claude->messages->createStream(
    model: $model,
    maxTokens: 4096,
    messages: [['role' => 'user', 'content' => $question]],
);

foreach ($stream as $event) {
    if ($event instanceof RawContentBlockDeltaEvent && $event->delta instanceof TextDelta) {
        echo 'data: ' . json_encode(['text' => $event->delta->text]) . "\n\n";
        flush();
    }
}

Event class and field names differ between SDK versions, so check the version you installed before copying them. The streaming part that breaks most often is not the SDK but buffering:

  • Call ob_end_flush() (or disable output_buffering) before the loop, and flush() after each chunk.
  • Behind nginx, send the X-Accel-Buffering: no header, or set fastcgi_buffering off for that location.
  • Send Content-Type: text/event-stream and Cache-Control: no-cache.
  • One streaming request holds one PHP-FPM worker for the whole answer. Size pm.max_children for the number of people who can stream at once, or serve streams from a separate pool.

To log tokens for a streamed call, ask for usage in the stream (OpenAI's Chat Completions needs stream_options with include_usage; the Responses API and Claude send usage in their final events) and write the log line when the stream ends.

How do you get reliable JSON out of an LLM in PHP?

Ask the API for structured output against a JSON schema instead of asking for JSON in the prompt and parsing free text. OpenAI supports JSON schema output with strict mode, and Claude supports structured outputs through output_config.format and strict tool definitions; the Anthropic SDK also has helpers that validate the answer against a schema class.

Even with a schema, validate the decoded data in PHP before it touches your database: check required keys, types and value ranges, and treat a failure like any other API error, with one retry and then a fallback.

What do you do with model output before you show or store it?

Treat it as untrusted input, the same as a form field. Our board asks the model for HTML that goes into a rich-text editor, and it never trusts that the model followed the instruction: it strips everything outside a short allowlist of tags (<p>, <h2>, <h3>, lists, <strong>, <em>, <code>, <br>) before saving. The same applies to Markdown that you render, SQL or code that a model suggests, and URLs it returns.

How do you cut OpenAI and Claude API costs from PHP?

Cache what you can, batch what nobody waits for, and choose the smallest model that passes your quality bar. The three levers, as documented by the providers on 29 October 2026:

Lever OpenAI Claude
Prompt caching Automatic for supported models; on current models the prompt needs at least 1,024 tokens; cached input is discounted up to 95% You mark the prefix with cache_control; minimum 512 to 4,096 tokens depending on the model; cache reads cost about 0.1x of input on most models, writes 1.25x (5-minute TTL) or 2x (1-hour TTL)
Batch API 50% off, results within 24 hours, up to 50,000 requests per batch 50% off every token, including cache reads and writes, results within 24 hours
Smaller model Route easy requests to a mini model Route easy requests to Haiku

Caching only works if the start of the prompt is byte-for-byte identical across requests. In practice that means: system prompt and tool definitions first and never changing, no timestamps or request IDs in them, the user's input last. Check that it works by reading the cached token count in usage: input_tokens_details.cached_tokens on OpenAI, cacheReadInputTokens in the Anthropic PHP SDK.

Our board has one cache of its own: text-to-speech audio is stored on disk under a key made from the text and the voice, so the same briefing is not paid for twice. A cache in your own code like this makes sense when the same input produces the same output and the output is expensive.

To estimate what a feature will cost per month before you build it, use our LLM API cost calculator, and for the pricing details behind it see OpenAI and Claude API cost.

What should you log for every LLM call?

Log one row per call with the feature name, model, prompt version, input tokens, cached tokens, output tokens, latency, status code, retry count and the user or tenant it belongs to. Do not log full prompts and answers by default if they contain personal data; log a hash or a sampled copy instead.

That row answers the questions that come up in the first month: which feature costs the most, whether caching works, whether a model change made answers slower, and which customer is responsible for a cost spike. With openai-php/client the token counts are in $response->usage; with the Anthropic SDK they are in $message->usage. Store the provider's request ID too (the x-request-id header on OpenAI, request-id on Anthropic), because it is the first thing provider support asks for.

The Anthropic PHP SDK also reports every call as an OpenTelemetry span when your application registers a tracer provider, which is enough if you already run tracing.

Lessons from our own PHP client

Three things from the board's code that are not in the API documentation:

  1. Whisper needs a file name with an extension. An uploaded file sits in PHP's temporary directory without an extension, and Whisper rejected it with HTTP 400 "Unrecognized file format". The fix was to pass the original file name and MIME type explicitly when building the multipart request.
  2. Keep the model name in configuration. The model comes from an environment variable with a default in code, so moving a feature to a newer or cheaper model is a config change and a deploy, not a code change.
  3. One class, one place to fix. All five features go through one client class. When we add the retry layer described above, it is one constructor change, not five.

Which approach do we recommend?

For most PHP teams, our verdict is:

  • Claude-first features: use the official anthropic-ai/sdk, pass it a configured HTTP client with your timeouts, and keep its default retries.
  • OpenAI-first features: use openai-php/client with a retrying PSR-18 client that retries POST on 429, 5xx and connection errors.
  • One or two simple features: your framework's HTTP client behind one service class is fine, but add retries before launch, not after the first incident.
  • Several providers or agents with tools: look at a library, compared in Laravel AI SDK vs Prism vs Symfony AI.

Whatever you choose, the checklist above does not change: timeouts per feature, retries that fire on POST, streaming without buffering, a stable prompt prefix, batch for offline work and one log row per call.

Next step

If you want a senior PHP team to add an OpenAI or Claude feature to your product, or to review an existing integration against this checklist, see our AI integration services. First features usually take 3 to 8 weeks and start from $10,000, with a fixed quote after a short scoping call. Contact us with a description of the feature and the data it uses.

Case studies

Frequently asked questions

Not from OpenAI. The standard choice is openai-php/client, a community package (v0.21.0 in September 2026, PHP 8.2+, about 2.9 million installs a month on Packagist), with Laravel and Symfony integration packages around it. Anthropic does publish an official PHP SDK, anthropic-ai/sdk (v1.3.0 on 23 October 2026, PHP 8.1+). Calling either REST API with your own HTTP client is also a reasonable choice for one or two features.

No. It sends the request through whatever PSR-18 HTTP client you give it and throws on errors. Retries have to come from that HTTP client, for example Symfony's RetryableHttpClient or a Guzzle retry middleware. Check what your retry layer does with POST requests: Symfony's default strategy, for example, does not retry a POST on a 500, a 504 or a connection timeout.

Set it per feature, not globally. Short classification or extraction calls can live with 20 to 30 seconds, generation of a long document may need 90 seconds or more, and anything longer belongs in a background job with streaming or the Batch API. Make sure PHP-FPM, the web server and any proxy allow at least that long, or the request dies upstream while PHP is still waiting.

Use the SDK's streamed method (createStreamed in openai-php/client, createStream in the Anthropic SDK) or the provider's server-sent events directly, write each chunk to the response and flush it. Turn off output buffering in PHP and in the web server; with nginx in front of PHP-FPM, send the X-Accel-Buffering: no header, otherwise the user sees nothing until the whole answer is ready.

On OpenAI, yes: caching is on by default for supported models once the prompt is long enough (1,024 tokens on current models), and you see the effect in the cached token count in usage. On Claude you mark the cacheable part with cache_control, and the prefix has to reach a model-specific minimum. In both cases the cached part must be byte-for-byte identical, so keep the system prompt and tool definitions fixed and put the changing user input last.

A first LLM feature in an existing PHP app usually takes us 3 to 8 weeks and starts from $10,000, with a fixed quote after a short scoping call. The model API bill is separate and paid by the client directly to the provider; we estimate it before launch, often with our LLM API cost calculator.

Let’s start your project
Book a call