API Reference
The pgEdge RAG Server provides a REST API for querying RAG pipelines.
Base URL
By default, the server listens on http://localhost:8080. All endpoints use
the /v1 API version prefix.
API Discovery
The server implements RFC 8631
for API documentation discovery. All JSON responses include a Link header:
Link: </v1/openapi.json>; rel="service-desc"
This allows tools like restish to automatically discover and use the API schema.
Endpoints
OpenAPI Specification
Get the OpenAPI v3 specification for the API.
GET /v1/openapi.json
Response
Returns an OpenAPI 3.0.3 specification document describing all API endpoints, request/response schemas, and error formats.
| Status Code | Description |
|---|---|
| 200 | OpenAPI specification |
Liveness Check
Check that the server process is up and serving. This is a cheap, dependency-free check that returns immediately and never contacts the LLM providers, so its latency is unaffected by provider health. It is the right endpoint for a Kubernetes liveness probe.
GET /v1/live
Response
{
"status": "ok"
}
| Status Code | Description |
|---|---|
| 200 | Server process is up |
Health Check
Check if the server is running, and whether each pipeline's embedding and completion providers are reachable.
GET /v1/health
Response
{
"status": "healthy",
"pipelines": [
{
"name": "my-docs",
"embedding": { "reachable": true },
"completion": { "reachable": true }
}
]
}
If a provider is unreachable, status becomes "degraded" and the
affected provider's entry includes an error:
{
"status": "degraded",
"pipelines": [
{
"name": "my-docs",
"embedding": { "reachable": true },
"completion": {
"reachable": false,
"error": "connection refused"
}
}
]
}
An unreachable provider does not change the HTTP status code — it
only degrades status in the body, so callers that just check for
HTTP 200 are unaffected.
Known cost caveat: connectivity is checked via the underlying
library's Ping, which is a free metadata call for OpenAI, Anthropic,
Gemini, and Ollama. For Voyage, Ping makes a real (tiny) embedding
request instead, since Voyage has no models-list endpoint. If a
pipeline's embedding_llm.provider is voyage, each /v1/health call
consumes a small amount of real Voyage API usage — worth accounting
for if health checks run frequently (e.g. a frequently polled probe).
Because it pings providers, /v1/health can take up to the provider
ping timeout (a few seconds) to respond, so it is better suited to a
readiness probe or to monitoring than to a latency-sensitive liveness
probe. For liveness, use /v1/live, which returns
immediately without contacting any provider.
| Status Code | Description |
|---|---|
| 200 | Server is running (see status in body) |
List Pipelines
Get a list of all available RAG pipelines.
GET /v1/pipelines
Response
{
"pipelines": [
{
"name": "my-docs",
"description": "Search my documentation"
},
{
"name": "knowledge-base",
"description": "Corporate knowledge base"
}
]
}
| Status Code | Description |
|---|---|
| 200 | List of pipelines |
Pipeline Stats
Get cumulative LLM token usage for every configured pipeline, broken
down by embedding and completion provider. Each figure is cumulative
since the underlying LLM client was created — a monotonically
increasing counter, not a per-request or windowed value. See the
known limitation below the example: embedding usage only
accumulates for pipelines using the Voyage provider.
GET /v1/stats
Response
{
"pipelines": [
{
"name": "my-docs",
"description": "Search my documentation",
"embedding": {
"prompt_tokens": 1024,
"completion_tokens": 0,
"total_tokens": 1024
},
"completion": {
"prompt_tokens": 4096,
"completion_tokens": 512,
"total_tokens": 4608
}
}
]
}
| Status Code | Description |
|---|---|
| 200 | Pipeline usage statistics |
Each embedding and completion object may also carry
cache_creation_input_tokens and cache_read_input_tokens fields.
These are omitted when zero, so they appear only for providers that
report prompt-cache usage (for example, an Anthropic completion
provider); the example above shows a pipeline with no cache activity.
Known limitation: embedding usage is sourced from the underlying
pgedge-go-llm-lib client, which currently only accumulates embedding
token usage for the Voyage provider. For pipelines whose
embedding_llm.provider is openai, gemini, or ollama, the
embedding field will always read zero, even though real embedding
calls are made and consume real tokens. completion usage is tracked
correctly for all providers. This was confirmed empirically against a
live HTTP round-trip and is a limitation in the shared library, not in
this endpoint.
Query Pipeline
Execute a RAG query against a specific pipeline.
POST /v1/pipelines/{name}
Path Parameters
| Parameter | Description |
|---|---|
name |
Pipeline name (from config) |
Request Body
{
"query": "How do I configure replication?",
"stream": false,
"top_n": 10,
"filter": {
"conditions": [
{"column": "product", "operator": "=", "value": "pgEdge"},
{"column": "version", "operator": "=", "value": "v5.0"}
],
"logic": "AND"
},
"include_sources": true,
"messages": [
{"role": "user", "content": "What is pgEdge?"},
{"role": "assistant", "content": "pgEdge is a distributed PostgreSQL platform..."}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
query |
string | Yes | The question to answer |
stream |
boolean | No | Enable streaming response (SSE) |
top_n |
integer | No | Override default result limit |
filter |
object | No | Structured filter to apply to results |
include_sources |
boolean | No | Include source documents (default: false) |
messages |
array | No | Previous conversation history for context |
The filter parameter accepts a structured filter object with conditions
and operators. This is useful when your data contains multiple products or
versions and you want to restrict results. API filters must use this
structured format for security (parameterized queries prevent SQL injection).
If the pipeline configuration also specifies a filter, both filters are combined using AND logic.
Filter examples:
Single condition:
{
"conditions": [
{"column": "product", "operator": "=", "value": "pgAdmin"}
]
}
Multiple conditions with AND:
{
"conditions": [
{"column": "product", "operator": "=", "value": "pgAdmin"},
{"column": "version", "operator": ">=", "value": "v8.0"}
],
"logic": "AND"
}
Multiple conditions with OR:
{
"conditions": [
{"column": "status", "operator": "=", "value": "published"},
{"column": "status", "operator": "=", "value": "draft"}
],
"logic": "OR"
}
Supported operators: =, !=, <, >, <=, >=, LIKE, ILIKE,
IN, NOT IN, IS NULL, IS NOT NULL
Message Object
| Field | Type | Description |
|---|---|---|
role |
string | Message role: user or assistant |
content |
string | Message content |
Non-Streaming Response
{
"answer": "To configure replication, you need to...",
"tokens_used": 1523
}
When include_sources: true:
{
"answer": "To configure replication, you need to...",
"sources": [
{
"id": "doc-123",
"content": "Replication is configured by...",
"score": 0.95
},
{
"id": "doc-456",
"content": "The replication settings include...",
"score": 0.87
}
],
"tokens_used": 1523
}
| Field | Type | Description |
|---|---|---|
answer |
string | The generated answer |
sources |
array | Source documents (only if requested) |
tokens_used |
integer | Total tokens consumed by the request |
Source Object
| Field | Type | Description |
|---|---|---|
id |
string | Document identifier (if available) |
content |
string | Document text content |
score |
number | Relevance score (higher is better) |
Streaming Response
When stream: true, the response uses Server-Sent Events (SSE).
Headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Event Format:
Each event is a JSON object sent as an SSE data line:
data: {"type": "chunk", "content": "To configure "}
data: {"type": "chunk", "content": "replication, "}
data: {"type": "chunk", "content": "you need to..."}
data: {"type": "done"}
Event Types
| Type | Description | Fields |
|---|---|---|
chunk |
Partial response content | content |
done |
Stream completed successfully | - |
error |
An error occurred | error |
Error Responses
{
"error": {
"code": "PIPELINE_NOT_FOUND",
"message": "pipeline not found: unknown-pipeline"
}
}
| Status Code | Error Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST |
Invalid request body or query |
| 404 | PIPELINE_NOT_FOUND |
Pipeline does not exist |
| 405 | METHOD_NOT_ALLOWED |
Wrong HTTP method |
| 500 | EXECUTION_ERROR |
Pipeline execution failed |
| 500 | INTERNAL_ERROR |
Unexpected server error |
Examples
cURL
List pipelines:
curl http://localhost:8080/v1/pipelines
Simple query:
curl -X POST http://localhost:8080/v1/pipelines/my-docs \
-H "Content-Type: application/json" \
-d '{"query": "How do I get started?"}'
Query with filter:
curl -X POST http://localhost:8080/v1/pipelines/my-docs \
-H "Content-Type: application/json" \
-d '{
"query": "How do I configure backups?",
"filter": {
"conditions": [
{"column": "product", "operator": "=", "value": "pgAdmin"},
{"column": "version", "operator": "=", "value": "v9.0"}
],
"logic": "AND"
}
}'
Streaming query:
curl -X POST http://localhost:8080/v1/pipelines/my-docs \
-H "Content-Type: application/json" \
-N \
-d '{"query": "Explain the architecture", "stream": true}'
Python
Non-streaming:
import requests
response = requests.post(
"http://localhost:8080/v1/pipelines/my-docs",
json={"query": "How do I configure SSL?"}
)
data = response.json()
print(data["answer"])
for source in data["sources"]:
print(f"- {source['content'][:100]}... (score: {source['score']:.2f})")
Streaming:
import requests
response = requests.post(
"http://localhost:8080/v1/pipelines/my-docs",
json={"query": "Explain the setup process", "stream": True},
stream=True
)
for line in response.iter_lines():
if line and line.startswith(b"data: "):
import json
event = json.loads(line[6:])
if event["type"] == "chunk":
print(event["content"], end="", flush=True)
elif event["type"] == "done":
print() # newline at end
JavaScript
Non-streaming:
const response = await fetch("http://localhost:8080/v1/pipelines/my-docs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "How do I get started?" }),
});
const data = await response.json();
console.log(data.answer);
Streaming with EventSource:
// Using fetch for SSE
const response = await fetch("http://localhost:8080/v1/pipelines/my-docs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: "Explain the setup", stream: true }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const event = JSON.parse(line.slice(6));
if (event.type === "chunk") {
process.stdout.write(event.content);
}
}
}
}
Rate Limiting
The server does not implement rate limiting. If needed, use a reverse proxy (nginx, Caddy, etc.) or API gateway in front of the server.
Authentication
The server does not implement authentication. For production deployments, place the server behind an authenticating proxy or API gateway.