Run on a Quack server
DuckDB 2.0 ships Quack, a
client-server protocol that lets one DuckDB instance serve others over HTTP. When a
client routes its session to the server with CONNECT, every query runs on the
server. The ai extension, its provider credentials and its outbound requests then
all live on the server, and clients need neither the extension nor any API key.
Start the server
Community extensions are not autoloaded, so the server has to load ai itself.
Put the setup in an init file:
-- ai-server.sql
INSTALL ai FROM community;
LOAD ai;
SET GLOBAL duckdb_ai_provider = 'openai';
Then start the server with the provider key in its environment:
OPENAI_API_KEY=... duckdb analytics.duckdb -init ai-server.sql -serve
-serve starts Quack on localhost:9494 and stores its token in a persistent
quack secret, so duckdb -connect on the same machine works without further
setup. To serve other machines, call quack_serve with allow_other_hostname
and put a TLS-terminating reverse proxy in front, as described in the
Quack security guide.
Instead of an environment variable, the key can live in a persistent secret on the server, which survives restarts:
CREATE PERSISTENT SECRET openai_server (
TYPE duckdb_ai, AI_PROVIDER 'openai', API_KEY '...'
);
Call AI functions from a client
Attach the server and route the session to it:
CREATE SECRET (TYPE quack, TOKEN '⟨server token⟩', SCOPE 'quack:ai-server.example:443');
ATTACH 'quack:ai-server.example:443' AS srv;
CONNECT srv;
SELECT ticket_id, ai_classify(body, ['billing', 'outage', 'other']) AS category
FROM support_tickets;
DISCONNECT;
While connected, every statement runs on the server, including SET duckdb_ai_*
and CREATE SECRET. A client can therefore pick models or bring its own
TYPE duckdb_ai secret for its session.
Where the call runs depends on how the client reaches the server:
| Client SQL | Where ai_* runs | Who needs the extension and a key |
|---|---|---|
CONNECT srv; SELECT ai_classify(body, ...) FROM t | Server | Server |
FROM srv.query('SELECT ai_classify(body, ...) FROM t') | Server | Server |
SELECT ai_classify(body, ...) FROM srv.t without CONNECT | Client | Client |
AI calls stop when the server interrupts the query, for example when the server's
max_execution_time passes or a client cancels the query with quack_cancel.
What clients share
Each CONNECTed client gets its own server session, but the extension's state is
per database instance:
ai_usage()lists the usage events of every client, andai_clear_usage()orai_clear_cache()from one client clears them for all.- The response cache, rate limits and token budgets are shared. All clients spend from the same provider account when they use the server's key.
Use Quack's authentication and authorization hooks to decide who may connect and which statements they may run.
Keep credentials and egress on the server
Remote clients can run any SQL the server allows, so the extension does not let a session decide where the server's keys or requests go:
- Keys stay with their origin. An API key from a
TYPE duckdb_aisecret is only sent to the scheme, host and port of theBASE_URLstored in that secret. Without one, the key, like a key from an environment variable, is only sent to the provider's endpoint: its built-in URL or the base URL environment variable (for exampleOPENAI_BASE_URL). A client that passesbase_url := ..., setsduckdb_ai_base_url, or creates an external model whoselocationpoints elsewhere gets an error, and no request is sent. - The environment allowlist is a ceiling. When the server process has
DUCKDB_AI_ALLOWED_HOSTSset, every provider and usage-log request must match it. A session can narrow it withduckdb_ai_allowed_hostsbut not widen it. Hosts in this variable may also receive the server's keys. enable_external_access = falseapplies. With external access disabled, the extension only sends requests to endpoints listed inallowed_directoriesorallowed_paths.
A locked-down server starts Quack in its init file before it disables external access, because Quack and persistent secrets are read from the home directory:
-- ai-server-locked.sql
INSTALL ai FROM community;
LOAD ai;
SET GLOBAL duckdb_ai_provider = 'openai';
CALL quack_serve();
SET allowed_directories = ['https://api.openai.com/'];
SET enable_external_access = false;
DUCKDB_AI_ALLOWED_HOSTS=api.openai.com OPENAI_API_KEY=... duckdb analytics.duckdb -init ai-server-locked.sql
enable_external_access cannot be turned back on for the running instance.
To send a key through a gateway, store the gateway URL in the secret, set the
provider's base URL environment variable, or list the gateway host in
DUCKDB_AI_ALLOWED_HOSTS:
CREATE PERSISTENT SECRET gateway (
TYPE duckdb_ai, AI_PROVIDER 'openai', API_KEY '...', BASE_URL 'https://ai-gateway.internal/v1'
);
See security and data flow for the full list of egress controls.