Baselyra Docs

AI assistant

Baselyra can talk to DeepSeek, or to anything that speaks the same /chat/completions shape. Two very different trust levels live behind /ai/v1: operator tooling that sees your real schema, and a plain relay any signed-in user may call. All of it is optional — without a key every route answers 503 and nothing else changes.

Enabling it

DEEPSEEK_API_KEY=sk-…
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
DEEPSEEK_MAX_TOKENS=2048
curl -s "$URL/ai/v1/status"
{"enabled":true,"model":"deepseek-chat"}

The key stays on the server. It is never sent to a browser and never appears in a response. DEEPSEEK_BASE_URL is any OpenAI-compatible /chat/completions endpoint, so a local Ollama or vLLM behind an OpenAI shim works if you point it there and set DEEPSEEK_MODEL to match.

The routes

RouteBodyWho
GET /ai/v1/statusAnyone
POST /ai/v1/sql{prompt}A Studio token or the service key
POST /ai/v1/explain{sql}A Studio token or the service key
POST /ai/v1/ask{question}A Studio token or the service key
POST /ai/v1/chat{messages, system?, stream?}Any signed-in application user

Bodies are capped at 256 KiB, a prompt at 8000 characters, and a conversation at 50 messages.

POST /ai/v1/sql

Generates a statement against your real schema. It is not run. It comes back into the SQL editor for you to read and execute.

curl -s -X POST "$URL/ai/v1/sql" \
  -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"prompt":"top 10 authors by published article count in the last 30 days"}'
{"sql":"SELECT u.id, u.email, count(*) AS articles FROM public.articles a JOIN auth.users u ON u.id = a.author WHERE a.published_at > now() - interval '30 days' GROUP BY 1, 2 ORDER BY articles DESC LIMIT 10",
 "explanation":"Counts articles published in the last month per author and returns the ten highest."}

The schema is rendered into the system prompt and the model is told to use only names that exist, to qualify every table, and to emit one statement with no trailing semicolon. A model that answers in prose instead of SQL gets its prose back as explanation with an empty sql — a wrong-shaped answer is still an answer, and showing it beats a 502.

POST /ai/v1/explain

curl -s -X POST "$URL/ai/v1/explain" \
  -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"sql":"delete from auth.sessions where expires_at < now()"}'
{"explanation":"This removes every row in auth.sessions whose expires_at is in the past — the refresh-token records for sign-ins that have already lapsed. It is destructive and cannot be undone, though the sessions it removes could no longer be refreshed anyway. It touches only auth.sessions."}

Two short paragraphs at most: what the statement returns or changes, which tables it touches, and anything expensive or destructive.

POST /ai/v1/ask

The only route that runs model-written SQL. It drafts a query, executes it, and answers the question from the rows.

curl -s -X POST "$URL/ai/v1/ask" \
  -H "authorization: Bearer $ADMIN_TOKEN" -H 'content-type: application/json' \
  -d '{"question":"how many users signed up last week?"}'
{"answer":"412 users signed up between 14 and 20 August.",
 "sql":"SELECT count(*) FROM auth.users WHERE created_at >= date_trunc('week', now() - interval '1 week') AND created_at < date_trunc('week', now())",
 "rows":[{"count":"412"}]}

If the model answers in prose rather than SQL — usually because the question was not about the data — the prose comes back as answer and no query runs.

How the query is contained. Four independent guards, because one hallucinated DROP TABLE is enough:

  1. SET TRANSACTION READ ONLY

    Every write and every DDL fails inside Postgres. Nothing relies on the server recognising a dangerous statement.

  2. A local statement_timeout of 10 seconds

    A cartesian join cannot pin a core.

  3. The statement is wrapped

    select * from (<statement>) as baselyra_ask limit 200. That bounds memory and, as a bonus, turns a multi-statement reply into a syntax error rather than a second query.

  4. The transaction is rolled back either way

    Nothing survives, even in principle.

POST /ai/v1/chat

A plain relay for your application's users, so an app built on Baselyra can offer its own assistant without standing up a second backend just to hold an API key. It requires a signed-in user token, not the anon key.

const { data, error } = await bl.ai.chat(
  [{ role: 'user', content: 'Summarise this order for me: ' + JSON.stringify(order) }],
  { system: 'You are a helpful support assistant for Acme. Be brief.' },
);
if (error) return show(error.message);
console.log(data.content);

Streaming, over server-sent events:

curl -N -X POST "$URL/ai/v1/chat" \
  -H "authorization: Bearer $USER_TOKEN" -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"hi"}],"stream":true}'
data: {"delta":"Hel"}
data: {"delta":"lo!"}
data: [DONE]

An error inside a stream arrives as a data: frame carrying the usual error object, because the status line has already been sent:

data: {"error":{"code":"ai_upstream","message":"DeepSeek returned 500","details":null}}

Cost control

ControlValue
/ai/v1/chat rate limit20 requests per minute per user — keyed on the user id, not the IP, so a mobile carrier NAT does not share one budget across thousands of people
DEEPSEEK_MAX_TOKENSA hard ceiling per completion. A caller may ask for fewer, never more.
Client disconnectAborts the upstream call, so you stop paying for tokens the moment the user closes the tab
Upstream timeout60 seconds, with one retry, and only for 429 and 5xx

There is no per-user or per-instance spend cap beyond these. If you expose /ai/v1/chat to the public, watch your DeepSeek dashboard.

Privacy

Be clear-eyed about what leaves the machine.

RouteSent upstream
/sqlYour schema — table, column and type names. No row data.
/explainThe statement you pasted. No row data.
/askYour schema, the question, and up to 200 result rows.
/chatExactly the messages your app sends.

Nothing is sent when DEEPSEEK_API_KEY is unset, which is the default. The AI routes themselves are not audited; a query you then run in the SQL editor is, like any other statement.

Errors

StatuscodeMeaning
503ai_disabledDEEPSEEK_API_KEY is not set
400bad_requestA malformed body — a missing prompt, a message over 8000 characters, an unknown role
401unauthorized/chat without a signed-in user
403forbidden/sql, /explain or /ask without a Studio token or the service key
429ai_rate_limitedDeepSeek's own rate limit, or Baselyra's per-user cap
502ai_unauthorizedDeepSeek rejected the key
502ai_upstreamDeepSeek returned an unexpected status
504ai_timeoutNo response within 60 seconds
504ai_unreachableA network failure reaching DeepSeek

In the Studio

Studio → AI carries the ask-your-database panel and SQL generation; the SQL editor offers "explain this" on the current statement. Every panel disappears when /ai/v1/status reports enabled: false, rather than showing a control that would return 503.

Failure modes

What you seeWhyFix
503 ai_disabledNo key in the running processSet DEEPSEEK_API_KEY and docker compose up -d
A stream that arrives all at once at the endA proxy is bufferingproxy_buffering off on nginx. The app already sends X-Accel-Buffering: no, but an explicit proxy_buffering on in your config wins.
/ask returns an answer and no sqlThe model answered in proseAsk a question about the data, or use /sql
/ask errors with a SQLSTATE and a sql in detailsThe generated statement did not run — usually an invented columnRead the statement; it is in the error
Generated SQL names a table that does not existThe schema in the prompt is a snapshot; a very new table may be missingRe-run after the catalog refreshes
/chat is 401 with a working anon keyThe relay needs a user token, not the project keySign the user in first

Edit this page Report a problem

Esc
navigate open Esc close