API Reference#
PullO exposes an OpenAI-compatible REST API for model inference. Any client that works with the OpenAI SDK works with PullO — just change the base_url and api_key.
Base URL#
https://pullo-9d1r.onrender.comAll inference endpoints are prefixed with /v1 for OpenAI compatibility.
Authentication#
Pass your API key as a Bearer token in the Authorization header:
Authorization: Bearer YOUR_API_KEYAPI keys are generated from the dashboard. They are scoped to a workspace and can be restricted to specific models, tools, and rate limits.
Keep your key secret
API keys grant access to your locally running models. Never commit keys to source control. Use environment variables or a secrets manager.
Each endpoint below includes examples in cURL, PowerShell, Python (httpx), and JavaScript (fetch).
Endpoints#
POST /v1/chat/completions#
Generate a chat completion using a locally running Ollama model. Fully OpenAI-compatible — works with any OpenAI SDK client.
Request
{
"model": "YOUR_MODEL_NAME",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Explain the pull-based tunnel model." }
],
"temperature": 0.7,
"max_tokens": 512,
"stream": false
}| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Ollama model name (e.g. llama3.2, mistral) |
messages | array | Yes | Array of {role, content} objects. content is a string, or an array of content parts for multimodal input (see below) |
temperature | float | No | Sampling temperature (0–2). Default 0.7 |
max_tokens | integer | No | Maximum tokens to generate |
stream | boolean | No | Enable SSE streaming (default false) |
session_id | string | No | Conversation memory session ID |
top_p | float | No | Nucleus sampling cutoff |
stop | string[] | No | Stop sequences |
cURL
curl -X POST https://pullo-9d1r.onrender.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_MODEL_NAME",
"messages": [
{"role": "user", "content": "Hello!"}
]
}'Python (httpx)
import httpx
client = httpx.Client(
base_url="https://pullo-9d1r.onrender.com",
timeout=300
)
headers = {
"Authorization": "Bearer YOUR_API_KEY"
}
resp = client.post(
"/v1/chat/completions",
headers=headers,
json={
"model": "YOUR_MODEL_NAME",
"messages": [
{"role": "user", "content": "Hello"}
],
},
)
print(resp.status_code)
print(resp.text) # useful for debugging
data = resp.json()
print(data["choices"][0]["message"]["content"])Python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(
base_url="https://pullo-9d1r.onrender.com/v1",
api_key="YOUR_API_KEY",
)
response = client.chat.completions.create(
model="YOUR_MODEL_NAME",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)JavaScript (fetch)
const resp = await fetch("https://pullo-9d1r.onrender.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "YOUR_MODEL_NAME",
messages: [{ role: "user", content: "Hello!" }],
}),
});
const data = await resp.json();
console.log(data.choices[0].message.content);PowerShell (curl.exe)
On Windows, use curl.exe (the real curl) with a PowerShell here-string (@'...'@) for the JSON body. Backticks (`) are PowerShell's line-continuation character — they let you spread the command across multiple lines for readability. The --data-raw flag prevents curl from interpreting escape sequences in the string.
$json = @'
{
"model": "YOUR_MODEL_NAME",
"messages": [
{
"role": "user",
"content": "Hello!"
}
]
}
'@
curl.exe -X POST "https://pullo-9d1r.onrender.com/v1/chat/completions" `
-H "Authorization: Bearer YOUR_API_KEY" `
-H "Content-Type: application/json" `
--data-raw $jsonTo inspect the full response, pipe through ConvertFrom-Json:
$json = @'
{
"model": "YOUR_MODEL_NAME",
"messages": [
{ "role": "user", "content": "Hello!" }
]
}
'@
$resp = curl.exe -s -X POST "https://pullo-9d1r.onrender.com/v1/chat/completions" `
-H "Authorization: Bearer YOUR_API_KEY" `
-H "Content-Type: application/json" `
--data-raw $json
$resp | ConvertFrom-Json | Select-Object -ExpandProperty choicesResponse
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1720000000,
"model": "YOUR_MODEL_NAME",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hi! How can I help you today?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 28,
"completion_tokens": 94,
"total_tokens": 122
}
}Streaming (SSE)
Set "stream": true to receive a Server-Sent Events stream:
cURL
curl -N -X POST https://pullo-9d1r.onrender.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "YOUR_MODEL_NAME", "messages": [{"role": "user", "content": "Count to 5"}], "stream": true}'Python (httpx)
import httpx
with httpx.stream(
"POST",
"https://pullo-9d1r.onrender.com/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"model": "YOUR_MODEL_NAME", "messages": [{"role": "user", "content": "Count to 5"}], "stream": True},
) as resp:
for line in resp.iter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
chunk = line[6:]
print(chunk)JavaScript (fetch)
const resp = await fetch("https://pullo-9d1r.onrender.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "YOUR_MODEL_NAME",
messages: [{ role: "user", content: "Count to 5" }],
stream: true,
}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
for (const line of text.split("\n")) {
if (line.startsWith("data: ") && line !== "data: [DONE]") {
console.log(JSON.parse(line.slice(6)));
}
}
}PowerShell (curl.exe)
$json = @'
{
"model": "YOUR_MODEL_NAME",
"messages": [
{ "role": "user", "content": "Count to 5" }
],
"stream": true
}
'@
curl.exe -N -X POST "https://pullo-9d1r.onrender.com/v1/chat/completions" `
-H "Authorization: Bearer YOUR_API_KEY" `
-H "Content-Type: application/json" `
--data-raw $jsonEach SSE event is a JSON delta:
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","choices":[{"delta":{"content":"The"},"index":0}]}
data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","choices":[{"delta":{"content":" pull"},"index":0}]}
data: [DONE]The stream terminates with data: [DONE]. The OpenAI JS/Python SDK handles this natively when stream=True.
Multimodal (Images & PDFs)#
content accepts OpenAI-style content parts for multimodal input. Attach files as base64-encoded data URLs — no uploads, no storage, nothing persisted.
{
"model": "YOUR_VISION_MODEL",
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "What's in this image?" },
{
"type": "image_url",
"image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." }
}
]
}
]
}Content part types
| Type | Payload | Description |
|---|---|---|
text | { text: string } | Plain text |
image_url | { image_url: { url } } | Base64 data URL (data:image/png|jpeg|webp|gif;base64,…). Requires a vision model (e.g. llava, moondream) |
file | { file: { filename?, file_data } } | Base64 PDF data URL (data:application/pdf;base64,…). Text is extracted server-side — works with any model, no vision support needed |
PDFs are text-extracted on the backend
PullO parses the PDF with the request and replaces the attachment with its extracted text before dispatch. This means PDF Q&A works on every model — including text-only ones. Scanned/image-only PDFs (no extractable text) are rejected with a clear error.
Privacy
Attachments exist only for the lifetime of the request. Raw base64 is never logged or stored — only request metadata (status, latency, token count) is recorded.
Limits
| Limit | Default | |---|---| | Max image size (decoded) | 10 MB | | Max PDF size (decoded) | 20 MB | | Max content parts per message | 8 | | PDF pages processed | first 50 |
Oversized payloads, unsupported MIME types, malformed base64, or unknown part types return 400 Bad Request with a descriptive message. Remote http(s) image URLs are not accepted — encode the file as base64.
Image example (Python)
import base64
import httpx
def as_data_url(path: str, mime: str) -> str:
with open(path, "rb") as f:
return f"data:{mime};base64,{base64.b64encode(f.read()).decode()}"
resp = httpx.post(
"https://pullo-9d1r.onrender.com/v1/chat/completions",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"model": "YOUR_VISION_MODEL", # e.g. llava
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Describe this screenshot."},
{"type": "image_url",
"image_url": {"url": as_data_url("screenshot.png", "image/png")}},
],
}],
},
timeout=300,
)
print(resp.json()["choices"][0]["message"]["content"])The same payload works with the OpenAI SDK — vision requests use the standard multimodal format:
from openai import OpenAI
client = OpenAI(
base_url="https://pullo-9d1r.onrender.com/v1",
api_key="YOUR_API_KEY",
)
response = client.chat.completions.create(
model="YOUR_VISION_MODEL",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What does this diagram show?"},
{"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}},
],
}],
)
print(response.choices[0].message.content)PDF example (cURL)
B64=$(base64 -w0 report.pdf)
curl -X POST https://pullo-9d1r.onrender.com/v1/chat/completions \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"YOUR_MODEL_NAME\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"Summarize this report.\"},
{\"type\": \"file\",
\"file\": {\"filename\": \"report.pdf\",
\"file_data\": \"data:application/pdf;base64,$B64\"}}
]
}]
}"Notes
- Text-only messages (plain strings) keep working exactly as before — multimodal parts are opt-in per message.
- If a message's parts reduce to text only (e.g. a PDF that got text-extracted), PullO sends it to the model as plain string content automatically.
- Images require the host model to support vision; otherwise the local runtime returns an error. Check your model on the Models page.
- Streaming (
stream: true) works with multimodal messages.
POST /v1/embeddings#
Generate text embeddings using a locally running embedding model.
Request
{
"model": "nomic-embed-text",
"input": "PullO exposes local Ollama models as OpenAI-compatible APIs."
}| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Embedding model (e.g. nomic-embed-text, mxbai-embed-large) |
input | string | string[] | Yes | Text or array of texts to embed |
encoding_format | string | No | "float" (default) or "base64" |
cURL
curl -X POST https://pullo-9d1r.onrender.com/v1/embeddings \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nomic-embed-text",
"input": "PullO exposes local Ollama models as OpenAI-compatible APIs."
}'Python (httpx)
resp = httpx.post(
"https://pullo-9d1r.onrender.com/v1/embeddings",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"model": "YOUR_MODEL_NAME",
"input": "PullO exposes local Ollama models as OpenAI-compatible APIs.",
},
)
print(resp.json())JavaScript (fetch)
const resp = await fetch("https://pullo-9d1r.onrender.com/v1/embeddings", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "YOUR_MODEL_NAMEt",
input: "PullO exposes local Ollama models as OpenAI-compatible APIs.",
}),
});
console.log(await resp.json());PowerShell (curl.exe)
$json = @'
{
"model": "YOUR_MODEL_NAME",
"input": "PullO exposes local Ollama models as OpenAI-compatible APIs."
}
'@
curl.exe -X POST "https://pullo-9d1r.onrender.com/v1/embeddings" `
-H "Authorization: Bearer YOUR_API_KEY" `
-H "Content-Type: application/json" `
--data-raw $jsonResponse
{
"object": "list",
"model": "nomic-embed-text",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.0023064255, -0.009327292, ...]
}
],
"usage": {
"prompt_tokens": 10,
"total_tokens": 10
}
}Error Codes#
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | Malformed JSON, missing required field, or invalid multimodal attachment (bad base64, unsupported type, size limit exceeded) |
401 | invalid_api_key | API key missing, malformed, or revoked |
403 | forbidden | API key does not have access to the requested model |
404 | model_not_found | Model does not exist or is not connected |
422 | model_offline | Model host machine is disconnected from PullO |
429 | rate_limit_exceeded | Too many requests — back off and retry |
500 | internal_error | Unexpected server error |
503 | host_disconnected | Extension disconnected mid-request |
504 | inference_timeout | Ollama did not respond within the configured timeout |
All errors return a JSON body:
{
"error": "Model 'YOUR_MODEL_NAME' is not currently connected.",
"request_id": "uuid-...",
"detail": "Model 'YOUR_MODEL_NAME' is not currently connected."
}Include the request_id when reporting issues.
Rate Limits#
Rate limits are applied per API key. Headers are included on every inference response:
X-Rate-Limit-Remaining: 5
Retry-After: 60| Tier | RPM | Daily budget |
|---|---|---|
| Free | 10 requests/min | 1,000 requests/day |
| Team | 60 requests/min | Unlimited |
RPM is enforced via a sliding window counter in Redis. Once exhausted, requests return 429 Rate limit exceeded with a Retry-After: 60 header.
Headers#
Every response includes these headers:
| Header | Description |
|---|---|
| X-Request-Id | Unique request ID for tracing and debugging |
| X-Rate-Limit-Remaining | Remaining requests in the current minute window |
Include the X-Request-Id when contacting support — it helps us trace the request through the system.
Client Libraries#
PullO's /v1 endpoints are fully OpenAI-compatible. Use any OpenAI SDK client — just change the base_url:
Python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(
base_url="https://pullo-9d1r.onrender.com/v1",
api_key="YOUR_API_KEY",
)
response = client.chat.completions.create(
model="YOUR_MODEL_NAME",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)JavaScript (OpenAI SDK)
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://pullo-9d1r.onrender.com/v1",
apiKey: "YOUR_API_KEY",
});
const response = await client.chat.completions.create({
model: "YOUR_MODEL_NAME",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);