Skip to content

External providers ​

An external provider is a hosted embedding API that postvec calls from the inference host. You drop a connector file into providers.d, bind a column to one of that file's model names, and enable() / search() / migrate() keep the same SQL as a local model.

The key lives in that file. PostgreSQL holds a path (postvec.providers_path), never a credential. Source text for a bound column is sent to the provider on every insert, update and query embed.

A connector file lives on the host that runs inference: the database host in embedded mode, each postvec-server process in remote mode. postvec.mode stays embedded or grpc.

ProviderTypical SQL nameServes
OpenAIopenai-text-embedding-3-smallEmbeddings
Coherecohere-embed-v4-0Embeddings
Amazon Bedrockaws-titan-embed-text-v2-0Titan embeddings
Geminigemini-embedding-001Embeddings
Mistralmistral-mistral-embedEmbeddings
OpenRouteropenrouter-openai-text-embedding-3-smallEmbeddings (OpenAI-shaped fronts)
UniVecunivec-baai-bge-m3Embeddings and hosted vector conversion

TOML, key sources and doctor checks: connector files.

Keys ​

ModeDirectory
Embedded/etc/postvec/providers.d on the database host
Remote (postvec-server)/opt/postvec/providers.d on each node (the engine root)

Files are 0600 in a 0700 directory, owned by the inference process account. provider add prompts with echo off, or takes --api-key-file, --api-key-env or --key-stdin. Pass the key through one of those; provider ls prints the source, not the value.

provider add TYPE --model ID writes the file, probes the key (one billed embed per model), reloads the running host and refreshes postvec.models. --no-verify skips the probe. If the host is down, the files are still written and are read at the next start.

bash
sudo postvec provider add openai --model text-embedding-3-small
postvec provider ls
sudo postvec doctor --database app

In a container the CLI runs as root, and the -local and postvec-server images set POSTVEC_PROVIDERS_PATH, so no sudo and no --path:

bash
docker exec -it postvec postvec provider add openai --model text-embedding-3-small

On a postvec-server host there is no cluster, so the command edits the files under /opt/postvec and checks no columns; --acknowledge-in-use stands in for that check:

bash
sudo postvec provider add openai --model text-embedding-3-small \
     --acknowledge-in-use --yes

Put the same connector files on every node in a fleet. Round-robin to a node that lacks one fails that request.

Bind a column ​

sql
SELECT postvec.enable('docs', 'body',
                      model => 'openai-text-embedding-3-small');
-- NOTICE:  postvec: model "openai-text-embedding-3-small" is served by
--          external provider "openai"; source text from column "body" will
--          be sent to that provider for embedding

enable(), adopt() and migrate() emit that NOTICE for a provider embed model. A UniVec hosted conversion emits a separate NOTICE that stored vectors will leave the host.

search() embeds the query from the PostgreSQL backend. In remote mode that embed is a gRPC call to a postvec-server node, so the deadline covers the network too. postvec.query_timeout_ms defaults to 2000 ms, which is tight for a hosted API:

sql
SET postvec.query_timeout_ms = 10000;

If the embed times out, postvec.search_degrade_to_fts (default on) returns lexical ranks only. To embed once in the application, call search_with_vector(). Writes use postvec.embed_timeout_ms (30 s).

Local and hosted columns coexist because each column names its own model.

Adding a key can change an existing column ​

A new route joins at the back of its space: a column already served there keeps its route until model prefer (or provider add --prefer) puts the new one first. A column bound to a route by name stays on it while it is served.

Columns whose route becomes this one

Route resolution prefers a direct embed over a converter. A column bound in a space nothing serves directly (one bridging through a converter, or bound to the new route's own name) starts sending source text to the provider on the next worker cycle. enable() stays as it is.

provider add and model prefer list those columns and wait for --acknowledge-in-use (or the typed answer). --yes confirms the write; this acknowledgement is separate.

Switch providers without a migration ​

A column bound to gemini-embedding-001 keeps embedding after the Google key is removed and an OpenRouter route for the same space is added. Resolution follows whatever route currently serves the space.

sudo postvec provider add openrouter --model google/gemini-embedding-001
sudo postvec model prefer gemini-embedding-001 openrouter-google-gemini-embedding-001

status() shows space, route and route_execution; the worker logs one line per column the first time it embeds through the new route. Nothing is re-embedded.

A database the command could not inspect is listed as UNKNOWN. Columns that keep their current route (a local model or an earlier provider route stays preferred) are omitted.

Adopt existing provider vectors ​

To keep searching an existing openai-text-embedding-ada-002 (or similar) column, name that space at adopt time and search the existing space. Stored rows stay. A provider key is optional on that path.

sql
SELECT postvec.adopt('docs', 'body',
                     vector_column => 'body_vec',
                     model => 'openai-text-embedding-ada-002',
                     backfill => 'none');

model is an assertion: a wrong name embeds later queries into the wrong space. Adopt lists the checks.

To have the provider embed new writes and queries, add the key first. That creates a direct embed route, so the column starts sending source text to the provider. provider add lists affected columns and asks first.

Move a column off a provider ​

sql
SELECT postvec.migrate('docs', 'body',
                       new_model => 'baai-bge-m3') AS migration_id \gset

SELECT state, rows_done, rows_total FROM postvec.migration_status(:migration_id);
-- until state = 'awaiting_finalize'

SELECT postvec.migration_finalize(:migration_id);

convert (default) translates stored vectors when a converter is installed. Without a converter, use strategy => 'reembed'. Change the stored model. Then:

bash
sudo postvec provider rm openai --acknowledge-in-use --yes

provider rm lists remaining columns first, then removes the connector file. Remove a referenced key file by hand if you also want the credential gone.

Failures ​

What happenedClassEffect
Network error, timeout, HTTP 408, 424, 429 or 5xxTransientBackoff and retry
HTTP 401, 402 or 403ConfigRetry. On remote, try another node. For UniVec, 402 means the account has no available credit
Unknown model id at the providerConfigRetry. On remote, try another node
Empty input, or a NULBad rowCaught before the request. That row goes to jobs_dead
Other HTTP 4xx, or a wrong count, dimension or indexPermanentThe batch goes to jobs_dead

Config retries until postvec.max_retries (default 5), then dead-letters. Fix the key and re-drive with retry_dead(). A migration retries Config until it succeeds.

A connector file that fails to load is isolated: the other files and the local models keep serving, and provider ls and doctor report the error.

Cost ​

Embedding providers usually bill per token. UniVec conversion bills per vector. postvec.max_document_bytes (1 MiB) dead-letters an oversized document before an embedding call. max_concurrent in the file caps in-flight requests. Set spend quotas on the provider.

Provider calls ignore postvec.embedded_max_inflight. A slow hosted call runs beside local ONNX.