For the Vercel AI SDK

Governed data tools for the Vercel AI SDK.

sqai.tools() adds exactly three read-only tools to generateText. The model discovers sources, submits one typed plan, and returns numbers computed by a deterministic engine — with hashes that replay every answer.

No API key required to startBuilt for the AI SDK Tools Registryai ^7.0.0 · zod ^4.0.0 · Node ≥ 20

IIInstall

One dependency line.

Server-side, Node 20 or newer — in Next.js, run it in a route handler with runtime = "nodejs". Peers are ai ^7.0.0 and zod ^4.0.0, the versions the plans are typed against. No account, no key, no config file.

IIIQuick start

First governed answer.

import { generateText, isStepCount } from 'ai';
import { createSQAI } from '@thyn-ai/sqai-ai-sdk';

const sqai = createSQAI({
  sources: [{ name: 'orders', path: './orders.csv' }],
});

const { text } = await generateText({
  model: 'anthropic/claude-sonnet-4.5',
  tools: sqai.tools(),
  stopWhen: isStepCount(12),
  prompt: 'Total revenue in the east region?',
});

createSQAI() connects lazily — nothing is read until the first tool call. sqai.tools() returns a typed SqaiToolSet, no cast needed. isStepCount(12) leaves the model room to discover, plan, and execute.

Add sources, not glue code.

CSV, JSON, in-memory records, and SQLite run in-process — nothing leaves your server. Postgres, Snowflake, BigQuery and the rest attach through the engine. Every source lands in the same typed shape, so downstream the tools can't tell a CSV from Snowflake. Every source, specified

const sqai = createSQAI({
  sources: [
    { name: 'orders', path: './orders.csv' },
    { name: 'sessions', provider: 'sqlite',
      path: './app.db', table: 'sessions' },
    { name: 'quotes', records: quotes },
  ],
});

Every option narrows.

Truncation is always declared; partial rows are never shown. The tool input the model fills carries no allowed* field — policy is fixed at createSQAI() time and checked in-process, before execution. A model cannot widen what it was given. The full governance model

const sqai = createSQAI({
  sources,
  allowedSources: ['orders'],
  allowedFields: { orders: ['region', 'revenue'] },
  allowedFunctions: 'all-readonly',
  defaultLimit: 100,
  maxRowsToModel: 25,
});
optiondefaulteffect
sourcesrequired

The only data the tools can see. Registered once — sources are immutable.

allowedSourcesall registered

Narrows which sources any plan may touch.

allowedFieldsall fields

Per-source column allow-list. Everything else is invisible.

allowedFunctions"all-readonly"

Capability allow-list — narrow-only. Naming anything wider throws unsupported_operation.

defaultLimit100

Rows returned when a plan doesn't set its own limit.

maxExecutionRows1,000

Hard cap on rows a single execution reads.

maxRowsToModel25

Rows the model is ever shown.

maxCellsToModel250

Cells the model is ever shown.

maxBytesToModel32,000

Bytes of result the model is ever shown.

IVThe toolbox

Exactly three tools.

Discovery, execution, rehearsal — one tool each. The surface never grows behind your back.

01

listSources

never executes

Returns each source's fields, types, and row counts, plus the operations every number field supports. Three detail modes keep token cost flat as sources multiply.

sum · avg · count · min · max · eq · in · gt · gte · lt · lte · is_null · is_not_null

02

queryData

the only one that executes

Takes one typed plan — a discriminated union, validated before anything runs. Four outcomes, never an exception: the loop always gets structure back, even when the answer is no.

kind: "query" | "computation" · version: "1"
ok · needs_clarification · rejected · error

03

explainQuery

dry run

Validates and resolves a plan without touching data. The invocation_hash it returns is a preview of what would run — not the hash of something that ran.

invocation_hash = preview ≠ executed

VOne run, printed

Real numbers from a real run.

generateText · one prompt · three tool steps

user

Total revenue in the east region — and the NPV of our revenue schedule at a 10% rate?

tool
listSources()orders · 12 rows · region, revenue
tool
queryData({ kind: "query", version: "1", … })sum(revenue) · region = "east"ok · 2130.50 · 5 ordersplan_hash f87610d8afeb…
tool
queryData({ kind: "computation", version: "1", … })finance.npv · rate 0.1 · values ← orders.revenueok · 3188.17 · 0.83 ms
assistant

East-region revenue is 2,130.50 across 5 orders. At a 10% rate, the NPV of the revenue schedule is 3,188.17.

The model never did arithmetic. Both numbers came off the deterministic engine — and the hash replays them.

VIVersus generic tools

Typed plans don't have bad days.

a generic SQL toolsqai.tools()
the model writesa generic SQL toola raw SQL stringsqai.tools()one typed plan — kind "query" or "computation", version "1", zod-validated
a malformed calla generic SQL toolthrows, retries, or silently no-opssqai.tools()returns needs_clarification with choices — the tools never throw
the same question twicea generic SQL tooldifferent SQL, different answerssqai.tools()the same plan, the same hash — replayable
the write patha generic SQL toolwhatever the connection allowssqai.tools()none by construction — 4,574 read-only capabilities, no model-reachable escape hatch
proofa generic SQL toolnone — a plausible numbersqai.tools()plan_hash and computation_hash on every answer

The failure modes on the left are filed, not imagined: vercel/ai #1905 · #2147 · #12020 · #6913

Design the full agent loop

Ship the toolbox.

No API key. No account. The first tool call just works.

Built for the AI SDK Tools Registry · ai ^7.0.0 · zod ^4.0.0 · Node ≥ 20 · server-side