Connect an AI provider
The providers config/ai.php knows, the two features that use them, and how to call one yourself.
The kit ships the Laravel AI SDK (laravel/ai, version 0.6.8) and a config/ai.php that already names fifteen providers. Two features use it: the automatic translation behind php artisan lang:sync, and the support team's assistant, which always talks to anthropic (the guide «Support assistant», for the support team). Read this to give that command a provider, to know which of the many keys in config/ai.php actually do something today, and to call a model from your own code without wiring anything new.
What is configured
config/ai.php has three parts.
Defaults. Which provider answers when a call does not name one, per kind of work. These are committed values, not environment variables; edit the file to change them.
| Key | Default | Used for |
|---|---|---|
default |
anthropic |
Text: agents and prompts. |
default_for_images |
gemini |
Image generation. |
default_for_audio |
openai |
Speech synthesis. |
default_for_transcription |
openai |
Speech to text. |
default_for_embeddings |
openai |
Vector embeddings. |
default_for_reranking |
cohere |
Reranking search results. |
Caching. caching.embeddings.cache is false; set it to true and the SDK stores generated embeddings in the cache store named by caching.embeddings.store (CACHE_STORE, default database).
Providers. Each entry pairs a driver with the environment variables that authenticate it. Setting a variable does nothing by itself: a provider is only called when it is the default for a kind of work or a call names it.
| Provider | Variables | Default |
|---|---|---|
anthropic |
ANTHROPIC_API_KEY, ANTHROPIC_URL |
URL https://api.anthropic.com/v1 |
azure |
AZURE_OPENAI_API_KEY, AZURE_OPENAI_URL, AZURE_OPENAI_API_VERSION, AZURE_OPENAI_DEPLOYMENT, AZURE_OPENAI_EMBEDDING_DEPLOYMENT, AZURE_OPENAI_IMAGE_DEPLOYMENT |
Version 2025-04-01-preview; deployments gpt-4o, text-embedding-3-small, gpt-image-1 |
bedrock |
AWS_BEARER_TOKEN_BEDROCK, AWS_BEDROCK_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_USE_DEFAULT_CREDENTIALS |
Region us-east-1; default credential provider on |
cohere |
COHERE_API_KEY |
|
deepseek |
DEEPSEEK_API_KEY |
|
eleven |
ELEVENLABS_API_KEY |
|
gemini |
GEMINI_API_KEY, GEMINI_URL |
URL https://generativelanguage.googleapis.com/v1beta/ |
groq |
GROQ_API_KEY |
|
jina |
JINA_API_KEY |
|
mistral |
MISTRAL_API_KEY |
|
ollama |
OLLAMA_API_KEY, OLLAMA_URL |
Key empty; URL http://localhost:11434 |
openai |
OPENAI_API_KEY, OPENAI_URL |
URL https://api.openai.com/v1 |
openrouter |
OPENROUTER_API_KEY |
|
voyageai |
VOYAGEAI_API_KEY |
|
xai |
XAI_API_KEY |
Only OPENAI_API_KEY appears in .env.example, empty. Apart from ANTHROPIC_API_KEY, which the support assistant needs, every other variable is read by config/ai.php and by nothing else in the kit: available, but not connected to any function. The bedrock entry shares AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with the S3 filesystem, so filling them for file storage also fills Bedrock's credentials, without anything calling Bedrock.
config/services.php holds no AI credentials: its entries are Mailgun, Postmark, Resend, SES, Telnyx, the Weblabor world service, Stripe and Facebook.
What the kit uses today
One path, from the command to the network:
php artisan lang:sync
→ App\Services\LangSyncService
→ App\Services\LangTranslatorService::translate()
→ App\Ai\Agents\TranslationAgent::make(locale: $locale)->prompt($text, provider: config('ai.default'))
lang:sync walks every locale in lang/ and, for each key that English has and the locale lacks, asks the agent for a translation. The agent's instructions tell the model to translate into the target locale, keep the meaning and tone, preserve placeholders, and return only the translated text. Before the call the service replaces every Laravel placeholder such as :name with a marker (__PH0__) and restores it afterwards, so a placeholder can never come back translated.
The provider is whatever default names in config/ai.php, anthropic as shipped. The model is the provider's cheapest text model, not its default one: the agent carries the SDK's #[UseCheapestModel] attribute, because a sync translates many short texts and the larger model costs more without translating them better. For Anthropic in this version of the SDK that is Haiku, claude-haiku-4-5-20251001. Change default and the translations follow that provider's own cheapest model. To choose another model without touching the agent, set the cheapest text model in a models block of the provider entry:
'anthropic' => [
'driver' => 'anthropic',
'key' => env('ANTHROPIC_API_KEY'),
'url' => env('ANTHROPIC_URL', 'https://api.anthropic.com/v1'),
'models' => [
'text' => ['cheapest' => 'claude-haiku-4-5-20251001'],
],
],
The command decides whether a provider is configured before the first call. The default provider counts as configured when its key is filled, or when its driver is ollama, which needs no key. Otherwise every missing key is written in English with the warning "AI translation provider is not configured", and the command still finishes: nothing else in lang:sync depends on the network. A call that fails or returns nothing is handled the same way, with the warning "Translation failed for", so a bad key or an exhausted quota leaves you with English text to translate by hand, never with a broken file.
So, to turn translation on with Anthropic:
ANTHROPIC_API_KEY=sk-ant-your-key
To translate with another provider instead, set its key and change default in config/ai.php to its name, for example 'default' => 'openai'. To translate locally with no key at all, install Ollama, pull a model, and set 'default' => 'ollama'; the cheapest model there is llama3.1:8b, and OLLAMA_URL points the SDK at another host.
The full translation workflow, including the languages a user can pick and the other lang:* commands, is in /help/languages-and-translations.
The second path is the support team's assistant. It does not read default: it always calls anthropic with ANTHROPIC_API_KEY and the model in FEATURE_ASSISTANT_MODEL, so changing the provider of the translations never moves it. The guide «Support assistant» explains it, and its settings are listed in Configure your project.
Two more things exist because the SDK was installed:
- Migration
2026_05_15_155312_create_agent_conversations_tablecreates the tablesagent_conversationsandagent_conversation_messages. They are the SDK's conversation store, for agents that remember a chat. The support assistant keeps its conversations there; nothing else writes to them. stubs/agent.stub,structured-agent.stub,agent-middleware.stubandtool.stubare the templatesmake:agent,make:agent --structured,make:agent-middlewareandmake:tooluse. The kit does not use them.
Nothing in the kit sends user data, content or prompts to any provider other than the translation strings above, only when you run lang:sync, and, while the support assistant is on, what it reads to answer the team's questions: the project's code and the results of its read-only database queries, with passwords, tokens and other credentials left out.
Call a provider from your own code
You do not need a client library or a service class. Generate an agent, give it instructions, and prompt it; the response's text is the model's answer.
php artisan make:agent SummaryAgent
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Promptable;
use Stringable;
class SummaryAgent implements Agent
{
use Promptable;
public function instructions(): Stringable|string
{
return 'Summarize the text you receive in one sentence. Return only the sentence.';
}
}
$summary = SummaryAgent::make()->prompt('The long text to summarize...')->text;
That call uses the default provider and its default text model. To pin a provider or a model, pass them to prompt() or declare them on the class:
use Laravel\Ai\Attributes\{Model, Provider};
use Laravel\Ai\Enums\Lab;
#[Provider(Lab::Anthropic)]
#[Model('claude-sonnet-4-6')]
class SummaryAgent implements Agent
{
// ...
}
// or per call
SummaryAgent::make()->prompt($text, provider: 'gemini', model: 'gemini-3-flash-preview');
Lab is the enum of the fifteen provider names; a plain string with the same name works too. stream() returns the answer as it is produced, queue() runs the prompt as a job. For a one-off prompt with no class, the agent() function builds an anonymous agent:
use function Laravel\Ai\agent;
$answer = agent('You answer in one word.')->prompt('What colour is the sky?')->text;
Images and embeddings have their own entry points, routed to default_for_images and default_for_embeddings:
use Laravel\Ai\{Embeddings, Image};
$image = Image::of('A lighthouse at dusk')->square()->generate();
$vectors = Embeddings::for(['first text', 'second text'])->generate();
In a test, SummaryAgent::fake(['a canned answer']) replaces the network with the answers you list, and Image::fake() and Embeddings::fake() do the same for theirs. php artisan agent:chat opens a conversation with one of your agents in the terminal, which is the quickest way to check a key works.
Follow the kit's own shape when you add one: keep the agent in app/Ai/Agents, call it from a model method or a service rather than from a Livewire component, and check the key before the call as LangTranslatorService does, so a missing provider degrades to a message instead of an exception.