Providers API
Providers are the backend adapters that agents and knowledge features use. There are four provider families: LLM providers (for chat/completion), embedding providers (for vector search), cross-encoder providers (for re-ranking), and semantic-search providers (pgvector, pgvectorscale, Lance).
How providers relate to tools and toolsets.
Configure an agent to use a provider.
Provider families and base paths
| Family | Base path | Supported backends |
|---|---|---|
| LLM | /v1/llm_providers |
anthropic, openresponses, openchat, gemini, ollama, openrouter |
| Embedding | /v1/embedding_providers |
huggingface, openai, gemini |
| Cross-encoder | /v1/cross_encoder_providers |
huggingface |
| Semantic search | /v1/ssp |
pgvector, pgvectorscale, lance |
LLM providers
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/llm_providers |
List LLM providers |
| POST | /v1/llm_providers |
Create an LLM provider |
| GET | /v1/llm_providers/{id} |
Get LLM provider by id |
| PUT | /v1/llm_providers/{id} |
Replace (full update) |
| DELETE | /v1/llm_providers/{id} |
Delete |
| POST | /v1/llm_providers/find |
Filter by predicate |
| GET | /v1/llm_providers/{id}/models |
List configured model names (row-cached) |
| POST | /v1/llm_providers/{id}/invalidate |
Drop the cached adapter |
| POST | /v1/llm_providers/_discover_models |
Probe a draft config for its model list |
LLM provider object
{
"id": "anthropic-prod",
"provider": "anthropic",
"models": [
{"name": "claude-sonnet-4-6", "context_length": 200000}
],
"config": {
"api_key": "***"
},
"limits": {
"max_concurrency": 4
}
}
Note on secrets: config.api_key (and equivalent fields for other providers) is never returned in plaintext. The GET and LIST responses replace the value with a masked string. The field is always present (not omitted) so you can confirm a key was stored.
| Field | Required | Description |
|---|---|---|
id |
no | Identifier (case-sensitive). If omitted, the server assigns a type-prefixed id (e.g. llm-provider-3f9a1c8d). Immutable after creation |
provider |
yes | Provider type: one of anthropic, openresponses, openchat, gemini, ollama, openrouter |
models |
yes | Non-empty list of {"name": string, "context_length": integer} entries |
config |
yes | Provider-specific connection config (discriminated by provider). API keys are accepted in write requests but masked on read. |
limits.max_concurrency |
yes | Max simultaneous in-flight requests (integer > 0) |
limits.request_timeout_seconds |
no | Per-event inactivity timeout for streaming calls (float, default 300.0). If no event arrives from the provider within this window the stream is aborted and the turn fails cleanly. Set to null to disable. See LLM stream timeout for guidance. |
LLM stream timeout
limits.request_timeout_seconds is a per-event inactivity timeout, not a total-generation cap. A long but progressing response is not killed; only a complete stall (no new event arriving) triggers it. The default 300 s covers most real-world runs. For local servers such as LM Studio on slower hardware you can lower this (e.g. 60 s) for faster failure detection; raise or disable (null) for very large models that have long gaps between tokens.
Create an LLM provider
POST /v1/llm_providers - returns 201 Created.
curl -X POST https://your-host/v1/llm_providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "anthropic-prod",
"provider": "anthropic",
"models": [{"name": "claude-sonnet-4-6", "context_length": 200000}],
"config": {"api_key": "sk-ant-..."},
"limits": {"max_concurrency": 4}
}'import httpx
r = httpx.post(
"https://your-host/v1/llm_providers",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "anthropic-prod",
"provider": "anthropic",
"models": [{"name": "claude-sonnet-4-6", "context_length": 200000}],
"config": {"api_key": "sk-ant-..."},
"limits": {"max_concurrency": 4},
},
)
assert r.status_code == 201const r = await fetch("/v1/llm_providers", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
id: "anthropic-prod",
provider: "anthropic",
models: [{name: "claude-sonnet-4-6", context_length: 200000}],
config: {api_key: "sk-ant-..."},
limits: {max_concurrency: 4}
})
})Errors: 422 if models is empty or config shape does not match provider. 409 on duplicate id.
Get configured model names
GET /v1/llm_providers/{id}/models returns the model names stored in the provider row. This endpoint is row-cached - it never calls the upstream LLM. It reflects the models list you configured, not a live discovery call.
curl https://your-host/v1/llm_providers/anthropic-prod/models \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get("https://your-host/v1/llm_providers/anthropic-prod/models",
headers={"Authorization": f"Bearer {token}"})
print(r.json()) # {"models": ["claude-sonnet-4-6"]}const r = await fetch("/v1/llm_providers/anthropic-prod/models", {
headers: {"Authorization": `Bearer ${token}`}
})
const {models} = await r.json()Response 200 OK:
{"models": ["claude-sonnet-4-6"]}
Errors: 404 if the provider id does not exist.
Invalidate cached adapter
POST /v1/llm_providers/{id}/invalidate drops the in-memory adapter for the given provider. Call this after a PUT to ensure the next request picks up the updated config. Returns 204 No Content - including when the id does not exist (no-op).
curl -X POST https://your-host/v1/llm_providers/anthropic-prod/invalidate \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post("https://your-host/v1/llm_providers/anthropic-prod/invalidate",
headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 204await fetch("/v1/llm_providers/anthropic-prod/invalidate", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`}
})Embedding providers
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/embedding_providers |
List embedding providers |
| POST | /v1/embedding_providers |
Create an embedding provider |
| GET | /v1/embedding_providers/{id} |
Get by id |
| PUT | /v1/embedding_providers/{id} |
Replace |
| DELETE | /v1/embedding_providers/{id} |
Delete |
| POST | /v1/embedding_providers/find |
Filter by predicate |
| GET | /v1/embedding_providers/{id}/models |
List configured model names (row-cached) |
| POST | /v1/embedding_providers/{id}/invalidate |
Drop cached embedder adapter |
| POST | /v1/embedding_providers/_discover_models |
Probe a draft config for its model list |
Embedding provider object
{
"id": "hf-embedder",
"provider": "huggingface",
"models": [
{"name": "sentence-transformers/all-MiniLM-L6-v2"},
{"name": "sentence-transformers/all-mpnet-base-v2"}
],
"config": {"token": "***"},
"limits": {"max_concurrency": 2}
}
| Field | Required | Description |
|---|---|---|
id |
no | Identifier (case-sensitive). If omitted, the server assigns a type-prefixed id (e.g. embedding-provider-3f9a1c8d). Immutable after creation |
provider |
yes | One of huggingface, openai, gemini |
models |
yes | Non-empty list of {"name": string} entries (no dim field at the row level) |
config |
yes | Provider-specific config (API token/key masked on read) |
limits.max_concurrency |
yes | Max in-flight requests |
Create an embedding provider
curl -X POST https://your-host/v1/embedding_providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "hf-embedder",
"provider": "huggingface",
"models": [{"name": "sentence-transformers/all-MiniLM-L6-v2"}],
"config": {"token": "hf-..."},
"limits": {"max_concurrency": 2}
}'import httpx
r = httpx.post(
"https://your-host/v1/embedding_providers",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "hf-embedder",
"provider": "huggingface",
"models": [{"name": "sentence-transformers/all-MiniLM-L6-v2"}],
"config": {"token": "hf-..."},
"limits": {"max_concurrency": 2},
},
)await fetch("/v1/embedding_providers", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
id: "hf-embedder",
provider: "huggingface",
models: [{name: "sentence-transformers/all-MiniLM-L6-v2"}],
config: {token: "hf-..."},
limits: {max_concurrency: 2}
})
})The /models and /invalidate sub-resources work identically to the LLM provider equivalents. After a PUT, call /invalidate to force the runtime to reload the updated config.
Cross-encoder providers
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/cross_encoder_providers |
List cross-encoder providers |
| POST | /v1/cross_encoder_providers |
Create |
| GET | /v1/cross_encoder_providers/{id} |
Get by id |
| PUT | /v1/cross_encoder_providers/{id} |
Replace |
| DELETE | /v1/cross_encoder_providers/{id} |
Delete |
| POST | /v1/cross_encoder_providers/find |
Filter by predicate |
| GET | /v1/cross_encoder_providers/{id}/models |
List configured model names |
| POST | /v1/cross_encoder_providers/{id}/invalidate |
Drop cached adapter |
Only the huggingface backend is supported. Object shape follows the same pattern as embedding providers (id, provider, models, config, limits). The id is optional on create: omit it and the server assigns a type-prefixed id (e.g. cross-encoder-provider-3f9a1c8d); it is immutable after creation.
curl -X POST https://your-host/v1/cross_encoder_providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "hf-reranker",
"provider": "huggingface",
"models": [{"name": "cross-encoder/ms-marco-MiniLM-L-6-v2"}],
"config": {"token": "hf-..."},
"limits": {"max_concurrency": 1}
}'import httpx
r = httpx.post(
"https://your-host/v1/cross_encoder_providers",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "hf-reranker",
"provider": "huggingface",
"models": [{"name": "cross-encoder/ms-marco-MiniLM-L-6-v2"}],
"config": {"token": "hf-..."},
"limits": {"max_concurrency": 1},
},
)await fetch("/v1/cross_encoder_providers", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
id: "hf-reranker",
provider: "huggingface",
models: [{name: "cross-encoder/ms-marco-MiniLM-L-6-v2"}],
config: {token: "hf-..."},
limits: {max_concurrency: 1}
})
})Semantic-search providers
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/ssp |
List semantic-search providers |
| POST | /v1/ssp |
Create |
| GET | /v1/ssp/{id} |
Get by id |
| PUT | /v1/ssp/{id} |
Replace |
| DELETE | /v1/ssp/{id} |
Delete |
| POST | /v1/ssp/find |
Filter by predicate |
| POST | /v1/ssp/{id}/invalidate |
Drop cached adapter |
Supported provider values: pgvector, pgvectorscale, lance. The id is optional on create: omit it and the server assigns a type-prefixed id (e.g. semantic-search-provider-3f9a1c8d); it is immutable after creation.
curl -X POST https://your-host/v1/ssp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "pgvec-main",
"provider": "pgvector",
"config": {"url": "postgresql://user:pass@db:5432/primer"}
}'import httpx
r = httpx.post(
"https://your-host/v1/ssp",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "pgvec-main",
"provider": "pgvector",
"config": {"url": "postgresql://user:pass@db:5432/primer"},
},
)await fetch("/v1/ssp", {
method: "POST",
headers: {"Authorization": `Bearer ${token}`, "Content-Type": "application/json"},
body: JSON.stringify({
id: "pgvec-main",
provider: "pgvector",
config: {url: "postgresql://user:pass@db:5432/primer"}
})
})Secret handling
API keys and tokens submitted in config are stored encrypted. GET and LIST responses always include the api_key (or equivalent) field but never return the plaintext value - the field contains a masked string. This applies to all provider families.
Errors note
All error responses use the RFC 7807 ProblemDetails envelope. Common status codes:
422- validation failed (e.g.modelslist is empty, orconfigshape does not matchprovider)409- duplicateid404- provider not found (also returned byGET /models; note thatPOST /invalidatereturns204even for missing ids)204- successful delete or invalidate (no body)