Runtime behavior
This page documents how the ai extension executes provider calls at runtime. It is
the operational reference for the provider hardening, caching, concurrency, and
egress controls used by the SQL functions.
Function stability
Model-backed scalar and aggregate functions are registered as VOLATILE in
DuckDB. The planner must not constant-fold or reuse their results as if provider
calls were deterministic.
Local helper functions such as ai_count_tokens() and
ai_is_read_only_sql() remain deterministic and do not call providers.
Prompt shape
Completion functions accept one user prompt plus an optional system prompt. Task wrappers and SQL assistant functions build provider messages from their SQL arguments: static instructions and schema context are placed in the system message when the selected provider protocol supports it, while row-specific text stays in the user message.
Multi-turn chat transcripts are deliberately outside the scalar function surface. Store or serialize the conversation in your own table and pass the conversation summary or transcript as the prompt when you need row-wise SQL enrichment.
Runtime state scope
Usage events, response-cache entries, and provider pacing state are scoped to the current DuckDB database instance. Separate embedded DuckDB databases in the same process do not share these buffers.
The affected state includes:
ai_usage()andai_clear_usage(),- opt-in response cache entries used by
cache := trueorduckdb_ai_cache, - bounded query-local embeddings used to deduplicate
ai_similarity()inputs, - generated SQL cache entries for successful
ai_query_data()binds, duckdb_ai_max_concurrent_requests,duckdb_ai_min_request_interval_ms, andduckdb_ai_token_limit_per_minute.
Intra-chunk provider concurrency
Row-wise provider scalar functions prepare row inputs, options, and secrets on the DuckDB execution thread, then submit provider work to a bounded executor owned by the current DuckDB database instance. Worker threads persist across vector chunks instead of being created once per vector. The DuckDB result vectors are filled after worker completion on the execution thread.
This applies to:
ai_complete(),ai_try_complete(),ai_complete_json(), andai_completion_request_json(),- task wrappers such as
ai_summarize(),ai_classify(),ai_redact(),ai_translate(), andai_filter(), ai_embed(),ai_embedding_request_json(), andai_similarity(), andai_sql().
The worker count is bounded by duckdb_ai_max_concurrent_requests or
max_concurrent_requests := ... when configured. Without an explicit cap, the
extension uses a conservative local worker count for the chunk while the
provider rate limiter still controls outbound request pacing.
Configured worker caps must be between 0 and 64; larger values are rejected
instead of being silently clamped.
SQL-assistant table functions perform one provider call per invocation.
Aggregate functions use one request for groups that fit max_context_chars and
a bounded parallel map/reduce request tree for larger groups.
Embedding request packing
ai_embed(), ai_similarity(), classifier training, and optimized
classification pack embedding inputs by provider/model option group. A request
is closed when the configured input count, estimated token count, or request
byte limit would be exceeded. External model options can override these
limits; conservative built-in defaults apply otherwise.
ai_similarity() deduplicates both sides of every row, keeps a bounded
query-local embedding cache across DuckDB vector chunks, embeds each distinct
value once, and computes cosine similarity locally. Its request count therefore
scales with packed distinct values rather than two HTTP requests per row. The
query cache is capped at 8 MiB and the database keeps at most eight recent query
caches; ai_clear_cache() clears it explicitly. Query-cache hits appear in
ai_usage_summary() without increasing batch_count.
When a provider rejects a multi-input embedding request with HTTP 413 or a recognized payload/context-size error, the extension recursively bisects that request. Recoverable splits do not count as terminal failures in usage summaries. Input packing measures the encoded JSON payload, so quotes, control characters, and other escaped content are included in the byte limit. A single input that exceeds an external model's declared context or byte limit fails explicitly.
Context-safe aggregate reduction
ai_agg() and ai_summarize_agg() never truncate grouped input. Groups within
max_context_chars use one final request. Larger groups are split on UTF-8-safe
boundaries, mapped concurrently into compact evidence, packed, and recursively
reduced until one final request fits. Every child uses the parent query's
operation tree for usage attribution. Set overflow_policy := 'error' to reject
an oversized group before any provider call. Hierarchical reduction also stops
immediately with an explicit error when a provider's intermediate responses do
not reduce either the chunk count or total byte size.
Cancellation and retries
Provider HTTP calls use libcurl with interrupt-aware progress callbacks. When a DuckDB query is interrupted, in-flight provider calls abort and retry sleeps are also interruptible.
Retries are disabled by default. When enabled with retry_count or
duckdb_ai_retry_count, retryable HTTP failures use exponential backoff with
jitter. HTTP Retry-After headers on provider responses take precedence over
the configured backoff.
Usage events keep one operation_id for all input events produced by a packed
request. Hierarchical aggregate children additionally carry a
parent_operation_id; retries retain the same operation ID and increment the
event's retry count. ai_usage_summary() exposes both row-event calls and
distinct request batches, plus bounded-buffer drop counters.
HTTP connection behavior
The extension initializes libcurl once per process, reuses a thread-local easy handle for provider requests, and attaches those handles to a shared libcurl connection cache. This allows libcurl to reuse connections across provider worker threads where the provider and libcurl build support it.
Provider calls use the configured timeout_seconds for the total request. The
connect timeout defaults to the smaller of 10 seconds and the total timeout. Set
connect_timeout_seconds := ..., duckdb_ai_connect_timeout_seconds, or
DUCKDB_AI_CONNECT_TIMEOUT_SECONDS to override it for provider requests.
Provider redirects are not followed. This avoids forwarding authorization or API-key headers to an unexpected redirect target.
Provider and usage-log destinations must use http or https. URLs containing
embedded credentials or raw control characters are rejected before libcurl is
called. The same control-character check applies to generated HTTP headers.
Provider and usage-log response bodies are limited to 64 MiB by default to
prevent an untrusted endpoint from growing an unbounded in-memory buffer. Set
DUCKDB_AI_MAX_RESPONSE_BYTES to a positive byte count, up to 1 GiB, when a
large batched embedding response requires a different bound.
CURLOPT_NOSIGNAL is enabled so DuckDB host processes are not exposed to
libcurl signal behavior during DNS or timeout handling.
Response caching
Response caching is opt-in:
SET duckdb_ai_cache = true;
SELECT ai_complete('Summarize this repeated prompt.');
SELECT ai_complete('Summarize this repeated prompt.');
SELECT * FROM ai_clear_cache();
The response cache is in-memory and scoped to the current DuckDB database instance. It is keyed by provider, model, endpoint, request payload, and response-relevant options. API keys are not stored directly in cache keys.
Cached responses still record usage events, but their elapsed time is reported
as 0 because no provider HTTP request was made.
The maximum number of cached entries defaults to 1024. Set
cache_max_entries := ..., duckdb_ai_cache_max_entries, or
DUCKDB_AI_CACHE_MAX_ENTRIES to change the bound. Use 0 to disable
response-cache storage. Cached response bodies and keys also have a hard 64 MiB
per-database bound; an individual response larger than that is not cached.
ai_query_data() also keeps a small in-memory generated-SQL cache for successful
binds. ai_clear_cache() clears the response, generated-SQL, and similarity
query caches.
Deterministic performance benchmark
The local benchmark uses a threaded mock embedding endpoint and reports request count, wall time, peak DuckDB thread count, peak resident memory, and dropped usage events for 1,000 and 10,000 rows. It also derives a provider cost estimate from mock token usage at a fixed price:
python3 test/benchmarks/ai_runtime_benchmark.py
It covers unique ai_embed() inputs and repeated ai_similarity() inputs. The
latter uses 20 distinct values at both row counts, making a regression from
query-level distinct-value scaling directly visible in http_requests.
Egress allowlisting
Use duckdb_ai_allowed_hosts or per-call allowed_hosts := ... to restrict
provider and usage-log destinations:
SET duckdb_ai_allowed_hosts = 'ai-gateway.internal,api.openai.com';
SELECT ai_complete(
'Summarize this.',
allowed_hosts := 'api.openai.com'
);
Entries can be hostnames, host:port, full URLs, wildcard subdomains such as
*.example.com, or *.
The allowlist check runs before the HTTP request, so disallowed hosts fail without sending a provider request.
JSON parsing
Provider responses and JSON Schema inputs are parsed with DuckDB's vendored
yyjson parser. This covers completion text extraction, token usage fields,
embedding arrays, ai_complete_json() validation, and
ai_complete_record() projection.
ai_complete_json() and ai_complete_record() intentionally implement a
documented JSON Schema subset rather than full JSON Schema draft parity.
Schema pattern checks use RE2-compatible regular expressions so validation
has bounded-time matching behavior; constructs unsupported by RE2 are rejected.