Configuration

Markdown

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 keyEnvDefault
defaultAI_DEFAULT_PROVIDERlocal
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:

SettingEnvDefault
Driver(fixed)openai (OpenAI-compatible API shape)
URLLOCAL_AI_URLhttp://127.0.0.1:1234/v1
API keyLOCAL_AI_API_KEYempty string
Default text modelLOCAL_AI_MODELconfig default local-model; .env.example sets openai/gpt-oss-20b
Translate modelLOCAL_AI_TRANSLATE_MODEL(empty — falls back to LOCAL_AI_MODEL)
LOCAL_AI_URL=http://127.0.0.1:1234/v1
LOCAL_AI_API_KEY=
LOCAL_AI_MODEL=openai/gpt-oss-20b
# Optional: faster non-thinking model for POST /ai/translate-fields (avoids nginx 504)
# LOCAL_AI_TRANSLATE_MODEL=phi-4-mini-instruct
# LOCAL_AI_REASONING_EFFORT=
# LOCAL_AI_CONTEXT_WINDOW=131072

LOCAL_AI_TRANSLATE_MODEL is an optional faster non-thinking model for AI field translation (POST /ai/translate-fields), so the blocking request does not hit an nginx 504. Example: phi-4-mini-instruct. See Translation workspace — AI translate.

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 keyTypical env vars
anthropicANTHROPIC_API_KEY, ANTHROPIC_URL
azureAZURE_OPENAI_API_KEY, AZURE_OPENAI_URL, AZURE_OPENAI_API_VERSION, deployment names
bedrockAWS_BEDROCK_REGION, AWS_BEARER_TOKEN_BEDROCK, AWS credentials / AWS_USE_DEFAULT_CREDENTIALS
cohereCOHERE_API_KEY
deepseekDEEPSEEK_API_KEY
elevenELEVENLABS_API_KEY
geminiGEMINI_API_KEY, GEMINI_URL
groqGROQ_API_KEY
jinaJINA_API_KEY
mistralMISTRAL_API_KEY
ollamaOLLAMA_API_KEY, OLLAMA_URL (default http://localhost:11434)
openaiOPENAI_API_KEY, OPENAI_URL
openrouterOPENROUTER_API_KEY
voyageaiVOYAGEAI_API_KEY
xaiXAI_API_KEY

These are not all listed in .env.example; they are read when present.

Assistant provider, reasoning and context window (1.4.0)

The in-app Assistant (AppAssistant) runs on config('ai.assistant.provider'), set by AI_ASSISTANT_PROVIDER (default local). It can differ from AI_DEFAULT_PROVIDER.

Each provider can set a reasoning effort (low, medium or high) and a context window. The Assistant turns the effort into the provider's own option:

ProviderEnv varDefaultSent as
openai (and Azure)OPENAI_REASONING_EFFORTmediumreasoning.effort with an automatic reasoning summary
anthropicANTHROPIC_REASONING_EFFORTmediumextended thinking with 2048 / 8192 / 24576 budget tokens
geminiGEMINI_REASONING_EFFORTmediumthinking_level with thought summaries
localLOCAL_AI_REASONING_EFFORT(unset = none)OpenAI-style reasoning.effort, only when set

Any other value, or an empty one, sends no reasoning option, so the model keeps its own behaviour. Higher effort means slower and more expensive answers; the reasoning the model returns is shown, collapsed, above the answer — AI chat — What an answer shows.

OPENAI_CONTEXT_WINDOW (400000), ANTHROPIC_CONTEXT_WINDOW (200000), GEMINI_CONTEXT_WINDOW (1048576) and LOCAL_AI_CONTEXT_WINDOW (131072) set the context size in tokens that the Assistant shows next to each answer's token usage. They are display only and do not cap the request; set the local one to your model's real context length.

AI_ASSISTANT_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
ANTHROPIC_REASONING_EFFORT=low

Remote import hosts

'remote_import_hosts' => array_values(array_filter(array_map(
    'trim',
    explode(',', (string) env('AI_REMOTE_IMPORT_HOSTS', '')),
))),
EnvDefaultMeaning
AI_REMOTE_IMPORT_HOSTSemptyComma-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 HMAC secret

ConfigEnvPurpose
webhook_tokenAI_WEBHOOK_TOKENHMAC-SHA256 key for POST /ai/webhooks/collection-import (not a Bearer)
webhook_token_previousAI_WEBHOOK_TOKEN_PREVIOUSPrevious key during rotation grace (optional)
webhook_token_previous_expires_atAI_WEBHOOK_TOKEN_PREVIOUS_EXPIRES_ATISO 8601 end of grace window

Clients must send X-AI-Webhook-Signature: sha256=<hex> where the digest is HMAC-SHA256 over the raw request body only (from 1.2.2, #214). Empty AI_WEBHOOK_TOKEN → 401 in every environment (1.3.0+; no local unsigned bypass); mismatch → 401. Bearer / X-AI-Webhook-Token alone is not enough. Rotate with ai:rotate-webhook-token — AI API.

CSRF stays excepted for this path (no session). Details + PHP snippet: AI API.

AI_WEBHOOK_TOKEN=generate-a-long-random-secret

Daily prompt limit

ConfigEnvDefault
daily_prompt_limitAI_DAILY_PROMPT_LIMIT0

0 means unlimited. When set to a positive integer, AiChatController enforces a per-user daily cap.

AI_DAILY_PROMPT_LIMIT=0

Embeddings

ConfigEnvDefaultRole
embeddings.enabledAI_EMBEDDINGS_ENABLEDfalseWhen true, SearchSimilarCollectionItems uses stored vectors (cosine over JSON float arrays; pgvector later). Item saves queue GenerateCollectionItemEmbeddingJob. Without stored vectors / on provider failure, text fallback (semantic-lite) still runs.
AI_EMBEDDINGS_ENABLED=false

Embedding caching block (separate from the feature flag):

'caching' => [
    'embeddings' => [
        'cache' => false,
        'store' => env('CACHE_STORE', 'database'),
    ],
],

MCP server

Optional first-party MCP endpoint at /mcp (externa-core 1.4.0). Full guide: MCP server.

ConfigEnvDefaultRole
mcp.enabledAI_MCP_ENABLEDfalse/mcp returns 404 while false
mcp.write_enabledAI_MCP_WRITE_ENABLEDfalseRegister MCP write tools (still need mcp:write, the write gate, approval)
mcp.rate_limit_per_minuteAI_MCP_RATE_LIMIT_PER_MINUTE60mcp limiter; a key's own rate_limit_per_minute wins when set
AI_MCP_ENABLED=false
AI_MCP_WRITE_ENABLED=false
AI_MCP_RATE_LIMIT_PER_MINUTE=60

Tool registry and write gate

Shared by chat and MCP (externa-core 1.4.0). Write and destructive tools are denied by default. Full guide: AI tool approvals & audit.

ConfigEnvDefaultRole
tools.enabledAI_TOOLS_ENABLEDtruefalse disables every AI tool
tools.write_enabledAI_WRITE_TOOLS (fallback AI_TOOLS_WRITE_ENABLED)falseKill switch for mutating tools
tools.write_allowAI_TOOLS_WRITE_ALLOWemptyCSV of mutating tool class names (merged with Settings → AI write tools)
tools.allowAI_TOOLS_ALLOWemptyWhen set, only these tool names are available
tools.denyAI_TOOLS_DENYemptyTool names that are never available
tools.approval_ttl_minutesAI_TOOLS_APPROVAL_TTL15Lifetime of MCP pending approvals
tools.audit_retention_daysAI_TOOLS_AUDIT_RETENTION_DAYS90ai:prune-tool-audit retention
tools.redact_keysAI_TOOLS_REDACT_KEYSbuilt-in listArgument keys redacted in the audit log and approval inbox
AI_TOOLS_ENABLED=true
AI_WRITE_TOOLS=false
AI_TOOLS_WRITE_ALLOW=
AI_TOOLS_APPROVAL_TTL=15
AI_TOOLS_AUDIT_RETENTION_DAYS=90
AI_TOOLS_ALLOW=
AI_TOOLS_DENY=

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
],
SettingMeaning
TablesLaravel AI conversation persistence uses agent_conversations and agent_conversation_messages on the default DB connection.
generate_titlefalse — 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
# LOCAL_AI_TRANSLATE_MODEL=phi-4-mini-instruct
AI_REMOTE_IMPORT_HOSTS=
AI_WEBHOOK_TOKEN=
AI_DAILY_PROMPT_LIMIT=0
AI_EMBEDDINGS_ENABLED=false
AI_MCP_ENABLED=false
# AI_WRITE_TOOLS=false