Prism PHP with a Custom OpenAI-Compatible Endpoint
Prism is the Laravel package for talking to model providers: you publish a config file, put credentials in .env, and call a fluent builder. Pointing it at a gateway is one config key, and the first thing to know is what that key already contains.
providers.openai.url defaults to https://api.openai.com/v1 — the version path is part of the default. That is the opposite convention from Spring AI, where base-url carries no version and a separate completions-path supplies it. Paste a versioned URL into Prism and it is correct; paste an unversioned one and every request 404s.
https://aicomp.ai/v1).
Create one free →
This page covers the config file, the two ways to override a URL per request, and the Laravel behaviour that makes an edit look like it did nothing.
The config file
php artisan vendor:publish --tag=prism-config
// config/prism.php
'providers' => [
'openai' => [
'url' => env('OPENAI_URL', 'https://api.openai.com/v1'),
'api_key' => env('OPENAI_API_KEY', ''),
],
],
OPENAI_URL=https://gw.example.com/v1
OPENAI_API_KEY=sk-your-gateway-key
Note what the fallback tells you: the default ends in /v1, so the value you put in OPENAI_URL is expected to end in /v1 too. The provider builds the operation path onto it, so a value of https://gw.example.com produces a request to /chat/completions where your gateway is listening on /v1/chat/completions.
Providers are selected by enum, not by string:
use Prism\Prism\Prism;
use Prism\Prism\Enums\Provider;
$response = Prism::text()
->using(Provider::OpenAI, 'gpt-5.6-terra')
->withPrompt('Explain quantum computing in two sentences.')
->asText();
Overriding per request, two ways that differ
// third argument to using()
Prism::text()
->using(Provider::OpenAI, 'gpt-5.6-terra', ['url' => 'https://gw.example.com/v1'])
->withPrompt('...')
->asText();
// or the dedicated method
Prism::text()
->using(Provider::OpenAI, 'gpt-5.6-terra')
->usingProviderConfig(['url' => 'https://gw.example.com/v1'])
->withPrompt('...')
->asText();
Both change the URL for that call. The difference is in what happens afterwards: usingProviderConfig() re-resolves the provider, so if you are switching endpoints per tenant or per request, that is the one that reliably rebuilds the client. The inline third argument is fine for a one-off on a provider that is already resolved.
The config cache hides your edit
This is the Laravel-specific trap, and it costs more time than the URL does.
Prism reads config/prism.php, and that file reads env(). In a deployed Laravel app the config is usually cached:
php artisan config:cache
Once cached, editing .env changes nothing — env() returns the cached value, and in some configurations returns null outright outside of config files. You change OPENAI_URL, reload, and requests still go to the old host, with no error that mentions caching.
php artisan config:clear # development
php artisan config:cache # re-build after changing .env in production
If the endpoint you configured is not the one receiving requests, clear the config cache before you touch the URL again.
The 30-second timeout is shorter than a gateway round trip
request_timeout defaults to 30 seconds and applies to every provider request unless you change it:
'request_timeout' => env('PRISM_REQUEST_TIMEOUT', 30),
A gateway that routes onward, or a large-context generation, can exceed that. The symptom is a client-side timeout rather than an API error — no status code from your gateway, nothing in its logs. Override globally with the config key, or per call:
Prism::text()
->using(Provider::OpenAI, 'gpt-5.6-terra')
->withClientOptions(['timeout' => 120])
->withPrompt('...')
->asText();
What server-side generation costs
Prism is a server-side library, so the shape here is a request-and-response rather than an agent loop: prompt in, completion out, with the tool loop only running when you attach tools. Input is what you send plus any conversation history you maintain yourself in your application.
A day of work on this page's workload is 200k input and 35k output tokens — 4M input and 0.7M output across 20 working days. That is mid-range for the tool matrix: larger than a chat client, smaller than a coding agent that re-reads a repository each step. See output vs input pricing for how the ratio picks the cheapest model.
| Model | Gateway rate in / out per 1M tokens | One day | 20 days |
|---|---|---|---|
| gpt-5.6-terra | $1.00 / $6.00 | $0.41 | $8.20 |
| claude-sonnet-5 | $1.00 / $5.00 | $0.38 | $7.50 |
| deepseek-v4-flash | $0.22 / $0.66 | $0.07 | $1.34 |
| gemini-3.7-flash | $0.375 / $1.875 | $0.14 | $2.81 |
| grok-4.1 | $1.00 / $5.00 | $0.38 | $7.50 |
One day = 200k input + 35k output on this page's workload. 20 days = 4M input and 0.7M output. Rates checked 2026-10-11 — gateway rates move with upstream promotions, so verify the current number in your dashboard before committing to a budget.
Verifying the route
- Include
/v1inOPENAI_URL. The default already has it, unlike Spring AI'sbase-url. - Clear the config cache after any
.envchange —php artisan config:clearlocally,config:cacheagain in production. - Address the provider by enum,
Provider::OpenAI, not a string. - Use
usingProviderConfig()when switching endpoints per request; the inline argument does not re-resolve the provider. - Raise
request_timeoutif you see client-side timeouts with nothing in the gateway's logs.
FAQ
FAQ
Should the URL include /v1?
Yes. The shipped default is https://api.openai.com/v1, and the provider appends the operation path. An unversioned URL 404s.
I changed .env and nothing happened
The config is cached. Run php artisan config:clear in development, or php artisan config:cache again after editing in production.
Why do requests time out with no error from my gateway?
request_timeout defaults to 30 seconds. Raise it in config/prism.php or pass ->withClientOptions(['timeout' => 120]).
When does the third argument to using() not apply?
When the provider has already been resolved. Use usingProviderConfig() instead, which re-resolves it.
Do I need to publish the config file?
Only to change defaults. php artisan vendor:publish --tag=prism-config creates config/prism.php; without it you are relying on the package defaults and environment variables.
Related
- Spring AI with a custom endpoint — the same server-side problem in Java, with the opposite /v1 convention
- Semantic Kernel — .NET and Python, where the answer differs per language
- OpenAI SDK with a custom endpoint — the client most of these libraries build on
- Tools that take one endpoint — the full matrix