The package reaches Cloudflare in one of four ways. Your config selects the route. Your calling code is the same on all of them. You can change route at any time by changing config.
Choose a route
| You want | Use |
|---|---|
| The shortest setup, and no gateway features | Direct route |
| AI Gateway logs, caching and rate limits | Gateway route |
Gateway features with one token, on api.cloudflare.com |
Direct route with a gateway header |
| OpenAI, Anthropic or another provider’s models, billed by Cloudflare | Direct route. See third-party models. |
| A proxy, or a URL that the package does not build | Custom url |
What each route needs
All routes need a Cloudflare API token with the Account, Workers AI, Read permission.
| Route | Provider config | Second token |
|---|---|---|
| Direct | key, account_id |
No |
| Gateway | key, account_id, gateway |
gateway_token, only if Authenticated Gateway is on |
| Direct with a gateway header | key, account_id, headers |
gateway_token, only on an authenticated gateway, and only for FLUX.2 |
| Custom | key, url |
Depends on the URL |
Values go in your application’s .env file. config/ai.php reads them with env().
Add to .env:
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_AI_API_TOKEN=your-api-tokenAdd to the providers array in config/ai.php:
'workers-ai' => [
'driver' => 'workers-ai',
'key' => env('CLOUDFLARE_AI_API_TOKEN'),
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
],Add to .env. Leave the gateway token empty unless Authenticated Gateway is on:
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_AI_API_TOKEN=your-api-token
CLOUDFLARE_AI_GATEWAY=your-gateway-slug
CLOUDFLARE_AI_GATEWAY_TOKEN=Add to the providers array in config/ai.php:
'workers-ai' => [
'driver' => 'workers-ai',
'key' => env('CLOUDFLARE_AI_API_TOKEN'),
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
'gateway' => env('CLOUDFLARE_AI_GATEWAY'),
'gateway_token' => env('CLOUDFLARE_AI_GATEWAY_TOKEN'),
],Add to .env:
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_AI_API_TOKEN=your-api-token
CLOUDFLARE_AI_GATEWAY=your-gateway-slugAdd to the providers array in config/ai.php:
'workers-ai' => [
'driver' => 'workers-ai',
'key' => env('CLOUDFLARE_AI_API_TOKEN'),
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
'headers' => ['cf-aig-gateway-id' => env('CLOUDFLARE_AI_GATEWAY')],
],Add to .env:
CLOUDFLARE_AI_API_TOKEN=your-api-token
CLOUDFLARE_AI_URL=https://your-proxy.example.com/ai/v1Add to the providers array in config/ai.php:
'workers-ai' => [
'driver' => 'workers-ai',
'key' => env('CLOUDFLARE_AI_API_TOKEN'),
'url' => env('CLOUDFLARE_AI_URL'),
],To change route, change .env and the provider entry. Your calling code stays the same.
If you set both url and account_id, the package throws. If you pass a URL as account_id or gateway, the package also throws, before it sends a request.
An empty gateway or gateway_token is ignored. So the Gateway entry above also works in an environment with no gateway, such as local development. Leave those two variables out of that environment’s .env.
Where each call goes
| Route | Text and embeddings | Run API calls |
|---|---|---|
| Direct | api.cloudflare.com/client/v4/accounts/{account}/ai/v1 |
api.cloudflare.com/client/v4/accounts/{account}/ai/run/{model} |
| Gateway | gateway.ai.cloudflare.com/v1/{account}/{gateway}/compat |
The direct run URL, plus the cf-aig-gateway-id header |
| Direct with a gateway header | The direct URL, plus the header | The direct run URL, plus the header |
url ending in /ai/v1 |
That URL | The /ai/v1 suffix becomes /ai/run/{model} |
url ending in /compat |
That URL | Throws. Set account_id instead. |
Any other url |
That URL | {url}/run/{model} |
Run API calls are Clef, Clairic, reranking, images, speech and transcription. They use a different URL because /compat and /ai/v1 do not serve those models. You do not need a second provider entry. One workers-ai entry handles all calls, and they go through your gateway when you set gateway.
Three calls do not follow this table:
- Non-JSON models with a gateway use
gateway.ai.cloudflare.com/v1/{account}/{gateway}/workers-ai/{model}. Markdown::of()calls/ai/tomarkdownonapi.cloudflare.com, or on yoururlif it ends in/ai/v1. It does not usegateway.CloudflareWebSearchcalls/ai/websearch/on the same host, and names its own gateway.
What works on each route
| Feature | Direct | Gateway | Direct with a gateway header |
|---|---|---|---|
| Text, tools, streaming, structured output | Yes | Yes | Yes |
| Embeddings | Yes | Yes | Yes. Tested on 2026-11-05. Cloudflare’s gateway docs do not list this endpoint. |
| Clef and Clairic | Yes | Yes | Yes |
| Reranking, images, speech, transcription | Yes | Yes, except nova-3 |
Yes, except nova-3 |
| Neuron event | Yes | Yes | Yes |
| Gateway logs, analytics, caching, rate limits | No | Yes | Yes |
| Authenticated Gateway | Not applicable | Needs gateway_token |
key is enough, except for FLUX.2 |
Third-party model with model: |
Yes | No. See below. | Yes |
Set the features that apply to a gateway, such as caching and rate limits, in the Cloudflare dashboard. The package does not set them.
Tokens on the gateway route
An authenticated gateway on gateway.ai.cloudflare.com needs two credentials on text and embeddings calls:
keygoes inAuthorizationand pays for the Workers AI call.gateway_tokengoes incf-aig-authorizationand admits the request to the gateway. It is an AI Gateway token with Run permission.
Without gateway_token, or with a wrong one, these calls return HTTP 401.
Run API calls with a JSON body go to api.cloudflare.com, which reads the API token from Authorization. They do not need gateway_token. Clef, Clairic, reranking, speech, Whisper models and JSON image models returned 200 this way on an authenticated gateway, tested on 2026-11-05. If you set the token, the package still sends the header, and the calls still work.
Non-JSON models
Cloudflare’s REST API rejects a non-JSON body through a gateway, so three kinds of model behave differently with a gateway. Tested on 2026-11-05.
| Model | Body | With a gateway |
|---|---|---|
@cf/black-forest-labs/flux-2-klein-4b, flux-2-klein-9b, flux-2-dev |
Multipart | Uses gateway.ai.cloudflare.com/v1/{account}/{gateway}/workers-ai/{model}. On an authenticated gateway, needs gateway_token on both routes. |
@cf/openai/whisper, @cf/openai/whisper-tiny-en |
Raw audio | Sends a JSON byte array instead. key is enough. JSON is about 4 times the size of the audio. |
@cf/deepgram/nova-3 |
Raw audio | Throws InvalidArgumentException. Cloudflare has no form of this request that works through a gateway. |
To use nova-3 in an app with a gateway, add a second provider entry with no gateway, and pass its name for that call. The call then skips the gateway, so its logs and limits do not apply.
'workers-ai-direct' => [
'driver' => 'workers-ai',
'key' => env('CLOUDFLARE_AI_API_TOKEN'),
'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
],Third-party models and the gateway route
The gateway route sends key as Authorization. Cloudflare forwards that header to the provider as a provider key, and it does not use a stored key. A Cloudflare token is not a valid OpenAI key, so the call returns HTTP 401 with OpenAI’s Incorrect API key provided. A gateway_token does not change this. Tested on 2026-11-05. Use the direct route for third-party models. See third-party models.
Model names
Write bare Workers AI IDs on every route, such as @cf/openai/gpt-oss-120b. The /compat path routes on a workers-ai/ prefix, and the package adds it. On any other URL, a workers-ai/ prefix throws on text calls. For Clef and Clairic, the package removes the prefix.
The gateway provider path
AI Gateway also has a workers-ai/v1 path. Set gateway_path to use it. It is not the default. On an authenticated gateway, chat and embeddings returned 200 on this path with gateway_token, tested on 2026-11-05. Tests on 2026-09-09 had 401 on embeddings on a gateway with authentication on, so /compat stays the default.