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.
| Provider | Typical SQL name | Serves |
|---|---|---|
| OpenAI | openai-text-embedding-3-small | Embeddings |
| Cohere | cohere-embed-v4-0 | Embeddings |
| Amazon Bedrock | aws-titan-embed-text-v2-0 | Titan embeddings |
| Gemini | gemini-embedding-001 | Embeddings |
| Mistral | mistral-mistral-embed | Embeddings |
| OpenRouter | openrouter-openai-text-embedding-3-small | Embeddings (OpenAI-shaped fronts) |
| UniVec | univec-baai-bge-m3 | Embeddings and hosted vector conversion |
TOML, key sources and doctor checks: connector files.
Keys
| Mode | Directory |
|---|---|
| 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.
sudo postvec provider add openai --model text-embedding-3-small
postvec provider ls
sudo postvec doctor --database appIn a container the CLI runs as root, and the -local and postvec-server images set POSTVEC_PROVIDERS_PATH, so no sudo and no --path:
docker exec -it postvec postvec provider add openai --model text-embedding-3-smallOn 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:
sudo postvec provider add openai --model text-embedding-3-small \
--acknowledge-in-use --yesPut the same connector files on every node in a fleet. Round-robin to a node that lacks one fails that request.
Bind a column
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 embeddingenable(), 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:
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-001status() 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.
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
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:
sudo postvec provider rm openai --acknowledge-in-use --yesprovider 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 happened | Class | Effect |
|---|---|---|
| Network error, timeout, HTTP 408, 424, 429 or 5xx | Transient | Backoff and retry |
| HTTP 401, 402 or 403 | Config | Retry. On remote, try another node. For UniVec, 402 means the account has no available credit |
| Unknown model id at the provider | Config | Retry. On remote, try another node |
| Empty input, or a NUL | Bad row | Caught before the request. That row goes to jobs_dead |
| Other HTTP 4xx, or a wrong count, dimension or index | Permanent | The 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.