Configuration
AI configuration
AI behaviour is configured in config/ai.php and consumed by laravel/ai plus Externa’s AppAssistant agent. Defaults target a local OpenAI-compatible gateway (LM Studio, vLLM, etc.).
Related
Env reference: Environment variables. HTTP routes: AI API. Product overview: AI assistant.
Default provider
| Config key | Env | Default |
|---|---|---|
default | AI_DEFAULT_PROVIDER | local |
default_for_images | (hard-coded) | gemini |
default_for_audio | (hard-coded) | openai |
default_for_transcription | (hard-coded) | openai |
default_for_embeddings | (hard-coded) | openai |
default_for_reranking | (hard-coded) | cohere |
AppAssistant is annotated with #[Provider('local')], so chat uses the local provider entry unless you change that attribute / default.
AI_DEFAULT_PROVIDER=local
Local provider (LOCAL_AI_*)
Under providers.local:
| Setting | Env | Default |
|---|---|---|
| Driver | (fixed) | openai (OpenAI-compatible API shape) |
| URL | LOCAL_AI_URL | http://127.0.0.1:1234/v1 |
| API key | LOCAL_AI_API_KEY | empty string |
| Default text model | LOCAL_AI_MODEL | config default local-model; .env.example sets openai/gpt-oss-20b |
LOCAL_AI_URL=http://127.0.0.1:1234/v1
LOCAL_AI_API_KEY=
LOCAL_AI_MODEL=openai/gpt-oss-20b
Tool calling
Prefer a model that supports tool calling reliably. Tiny models often hallucinate successful tool results without calling tools — Externa’s instructions tell the agent never to invent success, but a weak model still wastes time.
AppAssistant sets a 300s HTTP timeout and a high maxSteps() budget (field-type count + 16) for long tool loops.
Local gateway / Herd nginx
LM Studio may emit response.reasoning_text.* SSE events; Externa remaps them to OpenAI-shaped response.reasoning_summary_text.* so laravel/ai keeps the stream alive. If the browser still shows a mid-stream “network error”, raise nginx fastcgi_read_timeout (Herd default is often 60s) to at least 600s while the model thinks without emitting tokens.
Cloud / other providers
config/ai.php registers many providers. Set the matching env keys when you switch AI_DEFAULT_PROVIDER or call a named provider:
| Provider key | Typical env vars |
|---|---|
anthropic | ANTHROPIC_API_KEY, ANTHROPIC_URL |
azure | AZURE_OPENAI_API_KEY, AZURE_OPENAI_URL, AZURE_OPENAI_API_VERSION, deployment names |
bedrock | AWS_BEDROCK_REGION, AWS_BEARER_TOKEN_BEDROCK, AWS credentials / AWS_USE_DEFAULT_CREDENTIALS |
cohere | COHERE_API_KEY |
deepseek | DEEPSEEK_API_KEY |
eleven | ELEVENLABS_API_KEY |
gemini | GEMINI_API_KEY, GEMINI_URL |
groq | GROQ_API_KEY |
jina | JINA_API_KEY |
mistral | MISTRAL_API_KEY |
ollama | OLLAMA_API_KEY, OLLAMA_URL (default http://localhost:11434) |
openai | OPENAI_API_KEY, OPENAI_URL |
openrouter | OPENROUTER_API_KEY |
voyageai | VOYAGEAI_API_KEY |
xai | XAI_API_KEY |
These are not all listed in .env.example; they are read when present.
Remote import hosts
'remote_import_hosts' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('AI_REMOTE_IMPORT_HOSTS', '')),
))),
| Env | Default | Meaning |
|---|---|---|
AI_REMOTE_IMPORT_HOSTS | empty | Comma-separated host allowlist for ImportRemoteJson / related remote fetches. Empty → no remote hosts allowed. |
AI_REMOTE_IMPORT_HOSTS=api.example.com,data.partner.test
Security
Never leave remote import open to arbitrary hosts in production. Pair allowlists with short-lived Bearer tokens when the remote API requires auth.
Webhook token
| Config | Env | Purpose |
|---|---|---|
webhook_token | AI_WEBHOOK_TOKEN | Shared secret for POST /ai/webhooks/collection-import |
Auth accepts either:
Authorization: Bearer {token}, orX-AI-Webhook-Token: {token}
Compared with hash_equals against the configured value. If the env is empty, the webhook always returns 403.
CSRF is excluded for this path in bootstrap/app.php. Details: AI API.
AI_WEBHOOK_TOKEN=generate-a-long-random-secret
Daily prompt limit
| Config | Env | Default |
|---|---|---|
daily_prompt_limit | AI_DAILY_PROMPT_LIMIT | 0 |
0 means unlimited. When set to a positive integer, AiChatController enforces a per-user daily cap.
AI_DAILY_PROMPT_LIMIT=0
Embeddings and MCP
| Config | Env | Default | Role |
|---|---|---|---|
embeddings.enabled | AI_EMBEDDINGS_ENABLED | false | Toggles embeddings-related features (e.g. similar-item search tooling when wired). |
mcp.enabled | AI_MCP_ENABLED | false | Toggles MCP-related features. |
AI_EMBEDDINGS_ENABLED=false
AI_MCP_ENABLED=false
Embedding caching block (separate from the feature flag):
'caching' => [
'embeddings' => [
'cache' => false,
'store' => env('CACHE_STORE', 'database'),
],
],
Conversations tables
'conversations' => [
'connection' => env('DB_CONNECTION'),
'tables' => [
'conversations' => 'agent_conversations',
'messages' => 'agent_conversation_messages',
],
'generate_title' => false, // titles from first prompt via Str::limit
],
| Setting | Meaning |
|---|---|
| Tables | Laravel AI conversation persistence uses agent_conversations and agent_conversation_messages on the default DB connection. |
generate_title | false — Externa skips an extra LM call for titles; the first prompt is truncated instead. |
HTTP CRUD for conversations is under /ai/conversations/* (session auth + can-use-ai). See AI API.
Attachments cleanup
Expired chat attachments are purged by ai:cleanup-attachments (scheduled daily). Configure retention via attachment expires_at in application code; the command deletes expired AiChatAttachment rows and their files.
Minimal local AI .env
AI_DEFAULT_PROVIDER=local
LOCAL_AI_URL=http://127.0.0.1:1234/v1
LOCAL_AI_API_KEY=
LOCAL_AI_MODEL=openai/gpt-oss-20b
AI_REMOTE_IMPORT_HOSTS=
AI_WEBHOOK_TOKEN=
AI_DAILY_PROMPT_LIMIT=0
AI_EMBEDDINGS_ENABLED=false
AI_MCP_ENABLED=false