Browse documentation

Studio ships an AI assistant that can inspect your project — schemas, tables, indexes, RLS status, logs, metrics — and answer questions about it. It is bring-your-own-key: no traffic leaves your server until you configure a provider.

Providers

Configured in the ai.assistant settings group (Studio → Settings). GET /api/admin/ai/providers returns the catalogue, defined in packages/ai/src/providers/index.ts:

IdLabelDefault modelBase URL
anthropicAnthropic (Claude)claude-opus-5—
openaiOpenAIgpt-5—
geminiGoogle Geminigemini-2.5-pro—
deepseekDeepSeekdeepseek-chatprefilled: https://api.deepseek.com
qwenQwen (Alibaba Cloud)qwen-plusprefilled: https://dashscope-intl.aliyuncs.com/compatible-mode/v1
glmGLM (Z.ai)glm-4.6prefilled: https://api.z.ai/api/paas/v4
moonshotKimi (Moonshot AI)kimi-k2-0905-previewprefilled: https://api.moonshot.ai/v1
mistralMistral AImistral-large-latestprefilled: https://api.mistral.ai/v1
xaixAI (Grok)grok-4prefilled: https://api.x.ai/v1
groqGroqopenai/gpt-oss-120bprefilled: https://api.groq.com/openai/v1
openrouterOpenRouteropenrouter/autoprefilled: https://openrouter.ai/api/v1
compatibleOpenAI-compatible (custom base URL)—required

A prefilled base URL applies when the field is left blank, so picking a named provider and pasting its key is enough. Fill the field to override it, for example with a mainland-China endpoint (https://dashscope.aliyuncs.com/compatible-mode/v1 for Qwen, https://open.bigmodel.cn/api/paas/v4 for GLM) or a proxy. Use compatible for anything else that speaks the OpenAI API: Ollama, vLLM, LiteLLM.

Resolution order (services/admin/ai-config.service.ts):

  1. The project's own ai.assistant values, if enabled, provider and apiKey are all set.
  2. Otherwise the console-global config.
  3. Otherwise "not configured" — the assistant reports it and does nothing.

Setting enabled: false at project level with an API key present is an explicit opt-out: it does not fall through to the console default.

POST /api/admin/ai/test validates the resolved configuration.

What it can do

The tool registry (packages/ai/src/tools/registry.ts) is entirely read-mode today:

ToolReads
list_schemas, list_tables, list_indexesSchema layout
list_functions, list_triggers, list_extensionsDatabase objects
run_selectA single read-only query
rls_statusWhich tables have RLS on, and their policies
security_advisorAdvisor findings
read_logs, read_metrics, read_performanceObservability data
list_queuesQueue topics and delivery state
index_advisorIndex recommendations

Every tool call is an HTTP call to this server's own /api/admin/* endpoints, carrying the signed-in user's bearer token. Three consequences:

  • The assistant can do exactly what you can do, and nothing more.
  • Tool activity goes through the same guards and audit trail as your own clicks.
  • If the token is missing, tool calls simply fail with 401 — there is no service-token fallback.
Warning

run_select is defended in depth: statement-shape checks reject anything that is not a read, a function-call blocklist rejects pg_read_file, pg_sleep, dblink, lo_import, pg_terminate_backend and friends, and the query still runs through the /api/admin/sql/readonly endpoint inside a Postgres READ ONLY transaction.

Results are capped before being fed back to the model, so a single list_tables on a large database cannot flood the context window.

The run loop

runAgent in packages/ai/src/agent.ts is a bounded loop (maxSteps, default 12 from ai.assistant):

  1. Stream a provider turn; emit text_delta, tool_call, usage.
  2. On a provider error, the run ends immediately — no message is persisted and no end_turn is reported.
  3. No tool calls → done: end_turn.
  4. Read tools execute right away and the loop continues.
  5. Write tools are proposed, not executed. Every write in the turn is emitted as tool_proposal and the run ends, so you approve before anything mutates. autoApproveWrites in ai.assistant disables that gate.
  6. Hitting maxSteps emits truncated.

Transport

POST /api/admin/ai/chat is Server-Sent Events (text/event-stream, X-Accel-Buffering: no). Studio reads it with fetch + a stream reader because the browser cannot POST through EventSource. Closing the request aborts the run via AbortController.

Each frame is one JSON AgentEvent:

data: {"type":"thread","threadId":"…"}
data: {"type":"text_delta","text":"Your "}
data: {"type":"tool_call","call":{"id":"…","name":"list_tables","input":{}}}
data: {"type":"tool_result","callId":"…","name":"list_tables","result":[…]}
data: {"type":"done","stopReason":"end_turn"}

Errors are emitted as {"type":"error","message":…} followed by done. Only the caught error message is ever written to the stream — the resolved config (which carries your decrypted API key) is never yielded or thrown.

Conversations

Threads are stored in SQLite under AI_DATA_DIR (assistant.db), separate from BACKUP_DIR so restoring a database backup never touches conversations. Opening the store is deliberately non-fatal: on a read-only or unmounted volume you lose history, not your project API.

RouteAction
GET /api/admin/ai/threadsList threads in scope
GET /api/admin/ai/threads/:idRead one
PATCH /api/admin/ai/threads/:idRename
DELETE /api/admin/ai/threads/:idDelete

Scope is (project, userId). Every route that takes a thread id resolves ownership first and returns 404 Conversation not found otherwise — the store itself does no ownership check, so guessing an id gets you nothing.

Settings

See the ai.assistant group in the settings reference for the full field list, and @motherbase/ai for the package API.