Skip to content

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.

Works withLaravel AI SDK
On this page

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.

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.
  • A model id from /models. Use one that supports tool calling if your agents use tools (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.

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'),
                ],
            ],
        ],
    ],
];
.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.

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:

DocsVariable for a custom OpenAI URLopenai-compatible provider
Laravel 13.xOPENAI_URLDocumented. Env names in the shipped config/ai.php: OPENAI_COMPATIBLE_URL, OPENAI_COMPATIBLE_API_KEY.
Laravel 12.xOPENAI_BASE_URLNot 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, 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
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:

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 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
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 and read 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:

StatusExceptionTokens codes
402Laravel\Ai\Exceptions\InsufficientCreditsExceptioninsufficient_credits, no_funding, outstanding_debt, member_cap_reached
429Laravel\Ai\Exceptions\RateLimitedExceptionrate_limited, concurrency_limit, window_exhausted, model_limit_reached, rate_limit_exceeded
502, 503, 504Laravel\Ai\Exceptions\ProviderOverloadedExceptionupstream_unreachable, no_upstream_available, upstream_timeout
connection failureLaravel\Ai\Exceptions\ProviderConnectionExceptionnone; the request never reached Tokens
other 4xx and 5xxIlluminate\Http\Client\RequestExceptioninvalid_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). 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), but this page does not walk through that setup.

Troubleshooting#

SymptomCause and fix
The [tokens] openai-compatible provider requires a 'url' to be configuredurl is empty. Check the tokens entry and clear the config cache.
... requires a default text modelAdd models.text.default, or pass model: on the call.
404 model_not_foundThe model id is wrong. Copy it from /models.
404 on every call, error mentions a pathThe URL is missing /v1. Use https://tokens.bd/v1.
401 missing_api_key or invalid_api_keyTOKENS_API_KEY is empty in the running process, often because config is cached. Run php artisan config:clear.
RateLimitedExceptionRead error.code as shown above. rate_limited and concurrency_limit clear after Retry-After; window_exhausted clears at the plan reset.
InsufficientCreditsExceptionTop up in billing.
cURL error 28: Operation timed outThe timeout value is lower than the response time. Raise it.
Streamed text stops partwayThe total timeout was reached, or PHP's max_execution_time ended the request. Raise both.

Every Tokens error code is in Errors, and Troubleshooting has the general fixes.

Was this page helpful?

Still stuck? Open a support ticket

Need help configuring your agent?

Test your connection with the connection tester, or create an API key.