PullO
PullODocs
v1.0
PullO

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#

plaintext
https://pullo-9d1r.onrender.com

All inference endpoints are prefixed with /v1 for OpenAI compatibility.

Authentication#

Pass your API key as a Bearer token in the Authorization header:

plaintext
Authorization: Bearer YOUR_API_KEY

API 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

json
{
  "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
}
FieldTypeRequiredDescription
modelstringYesOllama model name (e.g. llama3.2, mistral)
messagesarrayYesArray of {role, content} objects. content is a string, or an array of content parts for multimodal input (see below)
temperaturefloatNoSampling temperature (0–2). Default 0.7
max_tokensintegerNoMaximum tokens to generate
streambooleanNoEnable SSE streaming (default false)
session_idstringNoConversation memory session ID
top_pfloatNoNucleus sampling cutoff
stopstring[]NoStop sequences

cURL

bash
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)

python
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"])
How to Run in VS Code
1/7

Open VS Code File New File, then save it as test.py (or any name ending in .py).

Step 1 of 7

Python (OpenAI SDK)

python
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)

javascript
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.

powershell
$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 $json

To inspect the full response, pipe through ConvertFrom-Json:

powershell
$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 choices

Response

json
{
  "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

bash
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)

python
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)

javascript
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)

powershell
$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 $json

Each SSE event is a JSON delta:

plaintext
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.

json
{
  "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

TypePayloadDescription
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)

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:

python
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)

bash
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

json
{
  "model": "nomic-embed-text",
  "input": "PullO exposes local Ollama models as OpenAI-compatible APIs."
}
FieldTypeRequiredDescription
modelstringYesEmbedding model (e.g. nomic-embed-text, mxbai-embed-large)
inputstring | string[]YesText or array of texts to embed
encoding_formatstringNo"float" (default) or "base64"

cURL

bash
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)

python
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)

javascript
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)

powershell
$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 $json

Response

json
{
  "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#

StatusCodeMeaning
400invalid_requestMalformed JSON, missing required field, or invalid multimodal attachment (bad base64, unsupported type, size limit exceeded)
401invalid_api_keyAPI key missing, malformed, or revoked
403forbiddenAPI key does not have access to the requested model
404model_not_foundModel does not exist or is not connected
422model_offlineModel host machine is disconnected from PullO
429rate_limit_exceededToo many requests — back off and retry
500internal_errorUnexpected server error
503host_disconnectedExtension disconnected mid-request
504inference_timeoutOllama did not respond within the configured timeout

All errors return a JSON body:

json
{
  "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:

plaintext
X-Rate-Limit-Remaining: 5
Retry-After: 60
TierRPMDaily budget
Free10 requests/min1,000 requests/day
Team60 requests/minUnlimited

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)

python
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)

javascript
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);