# Laravel AI SDK

> Use Tokens from the official Laravel AI SDK (laravel/ai) with the openai-compatible provider: config/ai.php, .env, a first call, streaming, tools, timeouts and error handling for Laravel 12 and 13.

The Laravel AI SDK (`laravel/ai`) is Laravel's official package for agents, text generation and other AI features. It has an `openai-compatible` driver that talks to any server with an OpenAI-style `/chat/completions` endpoint, which is what Tokens is. You declare a provider in `config/ai.php` with the Tokens URL and key, then pick it by name when you call an agent.

If you only want the plain OpenAI client for PHP, without Laravel's agent layer, see [PHP](/docs/php).

:::note[What was checked]
Based on the Laravel AI SDK documentation for Laravel 13.x and 12.x (laravel.com/docs, checked October 2026) and on the `laravel/ai` 1.2.0 source (released 7 October 2026). The 12.x page does not describe the `openai-compatible` driver, so that part comes from the 13.x docs and the package source. The code was checked against the documentation and source, not run end to end against Tokens.
:::

## What you need

- PHP 8.3 or newer and Laravel 12 or 13. `laravel/ai` 1.2.0 requires `php ^8.3` and `illuminate/* ^12.0|^13.0`.
- A Tokens key from [API keys](/docs/api-keys).
- A model id from [/models](/models). Use one that supports tool calling if your agents use tools ([Choosing a model](/docs/choosing-a-model)).

## Install

```bash
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
```

The publish step creates `config/ai.php` and a migration. The migration creates the `agent_conversations` and `agent_conversation_messages` tables that the SDK uses to store conversations. Run it even if you do not plan to use conversation memory yet.

If you already had `config/ai.php` from an older release, run `composer update laravel/ai` and compare your file with the current published one. The `openai-compatible` entry may be missing from an old copy, and you can add it by hand as shown next.

## Configure the Tokens provider

Add a provider to the `providers` array in `config/ai.php`. The name is yours to choose; this page uses `tokens`.

```php title="config/ai.php"
return [
    'default' => 'tokens',

    // ...

    'providers' => [
        // ... the providers that ship in the file stay as they are

        'tokens' => [
            'driver' => 'openai-compatible',
            'url' => env('TOKENS_BASE_URL', 'https://tokens.bd/v1'),
            'key' => env('TOKENS_API_KEY'),
            'models' => [
                'text' => [
                    'default' => env('TOKENS_MODEL', 'deepseek/deepseek-v4.1-flash'),
                ],
            ],
        ],
    ],
];
```

```ini title=".env"
TOKENS_API_KEY=tok_live_your_key
```

How each option works:

- **`url`** is required. Use `https://tokens.bd/v1` including `/v1`. The driver removes a trailing slash and posts to `chat/completions` under it, so the request goes to `https://tokens.bd/v1/chat/completions`.
- **`key`** is optional in the SDK and sent as a bearer token when present. For Tokens it is required.
- **`models.text.default`** is the model used when a call does not name one. Without it, a call that does not pass `model:` throws an `InvalidArgumentException` that says the provider needs a default text model.
- **`'default' => 'tokens'`** makes this provider the default for text. If you would rather leave the default alone, skip it and pass `provider: 'tokens'` on each call, as the examples below do.

:::warning[Keep the key out of config files]
Read the key with `env()` as above and keep `.env` out of version control. After you change `.env` or `config/ai.php` on a server that caches config, run `php artisan config:clear` (or `config:cache` again).
:::

### Laravel 12 and 13: the variable names differ, the config does not

The variable names in the Laravel docs differ between versions, and they only matter for the built-in `openai` provider:

| Docs | Variable for a custom OpenAI URL | `openai-compatible` provider |
| --- | --- | --- |
| Laravel 13.x | `OPENAI_URL` | Documented. Env names in the shipped `config/ai.php`: `OPENAI_COMPATIBLE_URL`, `OPENAI_COMPATIBLE_API_KEY`. |
| Laravel 12.x | `OPENAI_BASE_URL` | Not described on the page. The driver is in the `laravel/ai` package, which supports Laravel 12 and 13. |

A variable name is only whatever your `config/ai.php` passes to `env()`. The `tokens` entry above uses its own names, so it works on both versions. If you prefer the entry that already ships in the file, set `OPENAI_COMPATIBLE_URL=https://tokens.bd/v1` and `OPENAI_COMPATIBLE_API_KEY` in `.env` and add `models.text.default` to that entry.

### Why not the built-in openai provider

The built-in `openai` provider also accepts a custom `url`. In the package source it sends requests to `{url}/responses`, the OpenAI Responses API, while the `openai-compatible` driver sends them to `{url}/chat/completions`. Tokens serves both ([Chat Completions](/docs/chat-completions), [Responses](/docs/responses)). This page uses `openai-compatible` because it is the driver the docs describe for third-party gateways and it sends the Chat Completions format, which is the most widely supported one.

## Make a first call

An anonymous agent needs no class. Save this as a route or run it in `php artisan tinker`:

```php
use function Laravel\Ai\agent;

$response = agent(
    instructions: 'Answer in one short paragraph.',
)->prompt(
    'When should I use a queue instead of running code in the request?',
    provider: 'tokens',
    model: 'deepseek/deepseek-v4.1-flash',
);

echo $response->text;
```

`$response->text` is the reply. `(string) $response` gives the same text. `provider` accepts a plain string for a provider you named in `config/ai.php`. You can drop `model:` if `models.text.default` is set.

## Check that it works

First list the models the key can use, outside Laravel:

```bash
curl -s https://tokens.bd/v1/models -H "Authorization: Bearer $TOKENS_API_KEY"
```

Then, inside the app:

```bash
php artisan tinker
```

```php
\Laravel\Ai\agent(instructions: 'Reply with one word.')->prompt('Say ready.', provider: 'tokens')->text
```

A short reply means the URL, key and model are right. A `404 model_not_found` means the id is wrong; a `401` means the key did not arrive. See Troubleshooting below.

## An agent class

For anything reused, generate an agent class and pin its provider and model with attributes:

```bash
php artisan make:agent ReviewCoach
```

```php title="app/Ai/Agents/ReviewCoach.php"
namespace App\Ai\Agents;

use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Attributes\Timeout;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
use Stringable;

#[Provider('tokens')]
#[Model('deepseek/deepseek-v4.1-flash')]
#[Timeout(120)]
class ReviewCoach implements Agent
{
    use Promptable;

    public function instructions(): Stringable|string
    {
        return 'You review pull request descriptions and suggest one improvement.';
    }
}
```

```php
$response = (new \App\Ai\Agents\ReviewCoach)->prompt('Fix login redirect');
echo $response->text;
```

The `Provider`, `Model` and `Timeout` attributes are in the Laravel docs' agent configuration section. `Provider` accepts a `Lab` enum value, a string or an array.

## Stream responses

`stream()` returns a `StreamableAgentResponse`. Returning it from a route sends it to the browser as server-sent events:

```php title="routes/web.php"
use function Laravel\Ai\agent;

Route::get('/ask', function () {
    return agent(instructions: 'Answer briefly.')
        ->stream('Explain job batching in Laravel.', provider: 'tokens');
});
```

To handle the events yourself, for example in an Artisan command, loop over the stream:

```php
use Laravel\Ai\Streaming\Events\Error;
use Laravel\Ai\Streaming\Events\TextDelta;

$stream = agent(instructions: 'Answer briefly.')
    ->stream('Explain job batching in Laravel.', provider: 'tokens');

foreach ($stream as $event) {
    if ($event instanceof TextDelta) {
        echo $event->delta;
    } elseif ($event instanceof Error) {
        // $event->type holds the error code, $event->message the text
        fwrite(STDERR, "{$event->type}: {$event->message}\n");
    }
}
```

The Laravel docs also describe a `then()` callback that runs when the whole response has been streamed; its `StreamedAgentResponse` carries `text`, `events` and `usage`. The driver asks Tokens for `stream_options.include_usage`, so token counts arrive at the end of the stream. See [Streaming](/docs/streaming) for the wire format.

## Tools

Tools are classes with a description, a schema and a `handle` method. Generate one:

```bash
php artisan make:tool GetWeather
```

```php title="app/Ai/Tools/GetWeather.php"
namespace App\Ai\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Ai\Contracts\Tool;
use Laravel\Ai\Tools\Request;
use Stringable;

class GetWeather implements Tool
{
    public function description(): Stringable|string
    {
        return 'Current weather for a city.';
    }

    public function handle(Request $request): Stringable|string
    {
        return $request['city'].': light rain, 29C'; // call your real weather source here
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'city' => $schema->string()->required(),
        ];
    }
}
```

Pass it to an anonymous agent, or return it from the `tools()` method of an agent class:

```php
use App\Ai\Tools\GetWeather;
use function Laravel\Ai\agent;

$response = agent(
    instructions: 'Use the weather tool when asked about weather.',
    tools: [new GetWeather],
)->prompt('Is it raining in Dhaka?', provider: 'tokens');

echo $response->text;
```

The SDK sends the tool definitions in the Chat Completions format and runs the loop for you. Tool calling only works on models that support it, so check the model page in [/models](/models) and read [Tool calling](/docs/tool-calling).

## Timeouts

The agent timeout defaults to 60 seconds. Raise it per call, per class, or both:

```php
$response = $agent->prompt('...', provider: 'tokens', timeout: 180);
```

For a class, use `#[Timeout(180)]`. The value is the HTTP client timeout in seconds for the request. In Laravel's HTTP client it is Guzzle's `timeout` option, which Guzzle describes as the total time for the request. A long streamed answer can therefore be cut at that limit, so set it above your longest expected generation.

Other limits to know:

- The gateway waits up to 600 seconds for response headers from the upstream, so long generations are fine on its side.
- PHP itself can stop a web request first. Check `max_execution_time` in `php.ini` (the PHP default for web requests is 30 seconds), or run long jobs in a queue.

## Handle errors

The SDK maps some HTTP statuses to its own exceptions. For Tokens:

| Status | Exception | Tokens codes |
| --- | --- | --- |
| 402 | `Laravel\Ai\Exceptions\InsufficientCreditsException` | `insufficient_credits`, `no_funding`, `outstanding_debt`, `member_cap_reached` |
| 429 | `Laravel\Ai\Exceptions\RateLimitedException` | `rate_limited`, `concurrency_limit`, `window_exhausted`, `model_limit_reached`, `rate_limit_exceeded` |
| 502, 503, 504 | `Laravel\Ai\Exceptions\ProviderOverloadedException` | `upstream_unreachable`, `no_upstream_available`, `upstream_timeout` |
| connection failure | `Laravel\Ai\Exceptions\ProviderConnectionException` | none; the request never reached Tokens |
| other 4xx and 5xx | `Illuminate\Http\Client\RequestException` | `invalid_api_key`, `model_not_found`, `model_not_allowed_on_key` and the rest |

The Tokens `code` is in the response body. The SDK exceptions keep the original error as the previous exception:

```php
use Illuminate\Http\Client\RequestException;
use Laravel\Ai\Exceptions\AiException;

try {
    $text = agent(instructions: 'Say hi.')->prompt('hi', provider: 'tokens')->text;
} catch (AiException|RequestException $e) {
    $request = $e instanceof RequestException ? $e : $e->getPrevious();
    $response = $request instanceof RequestException ? $request->response : null;

    logger()->warning('Tokens call failed', [
        'status' => $response?->status(),
        'code' => $response?->json('error.code'),
        'request_id' => $response?->header('x-tokens-request-id'),
    ]);

    throw $e;
}
```

Log `x-tokens-request-id` with every failure. Support can trace a request from it ([Errors](/docs/errors)). Retrying `429 window_exhausted` does not help until the window resets; `Retry-After` says when.

## What the openai-compatible driver covers

In the package source the driver implements text generation (with streaming and tools), embeddings and transcription. Image generation and text-to-speech go through other providers in the SDK, not this one. Embeddings need `models.embeddings.default` in the provider config; Tokens serves embeddings ([Embeddings](/docs/embeddings)), but this page does not walk through that setup.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| `The [tokens] openai-compatible provider requires a 'url' to be configured` | `url` is empty. Check the `tokens` entry and clear the config cache. |
| `... requires a default text model` | Add `models.text.default`, or pass `model:` on the call. |
| `404 model_not_found` | The model id is wrong. Copy it from [/models](/models). |
| `404` on every call, error mentions a path | The URL is missing `/v1`. Use `https://tokens.bd/v1`. |
| `401 missing_api_key` or `invalid_api_key` | `TOKENS_API_KEY` is empty in the running process, often because config is cached. Run `php artisan config:clear`. |
| `RateLimitedException` | Read `error.code` as shown above. `rate_limited` and `concurrency_limit` clear after `Retry-After`; `window_exhausted` clears at the plan reset. |
| `InsufficientCreditsException` | Top up in [billing](/dashboard/billing). |
| `cURL error 28: Operation timed out` | The `timeout` value is lower than the response time. Raise it. |
| Streamed text stops partway | The total timeout was reached, or PHP's `max_execution_time` ended the request. Raise both. |

Every Tokens error code is in [Errors](/docs/errors), and [Troubleshooting](/docs/troubleshooting) has the general fixes.

---
Page: https://tokens.bd/docs/laravel-ai-sdk
