Capabilities

Chat

Chat is the core of Pendra: send a list of messages, get a reply. It powers assistants, summarisation, classification, code generation, and anything else you'd reach for a language model to do. Pendra is OpenAI-compatible, so if you've written against the OpenAI Chat Completions API before, the shapes are identical — point your client at https://api.pendra.ai with a pdr_sk_… key and you're running.

Send a message

Every request carries a model and an array of messages. Each message has a rolesystem (instructions), user (the prompt), or assistant (the model's previous turns) — and content. Pass the whole conversation back on each call; Pendra is stateless and doesn't remember prior turns for you.

from pendra import Pendra

client = Pendra()  # reads PENDRA_API_KEY

response = client.chat.completions.create(
    model="qwen3.6:27b",
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Explain GPUs in one sentence."},
    ],
)
print(response.choices[0].message.content)
Response

A GPU is a specialised processor built for the massively parallel arithmetic that graphics and, more recently, AI workloads depend on.

Stream the reply

Set stream: true to receive tokens as they're generated rather than waiting for the whole reply. Pendra returns Server-Sent Events in OpenAI's format, flushed immediately so text appears as it's produced. Streaming is the right default for any interactive UI, since text appears as it's produced. (Non-streaming requests are fine for long generations too — Pendra keeps the connection alive for you up to ~30 minutes.)

stream = client.chat.completions.create(
    model="qwen3.6:27b",
    stream=True,
    messages=[{"role": "user", "content": "Write a haiku."}],
)
for event in stream:
    if not event.choices:  # usage / timing chunk — no text
        continue
    print(event.choices[0].delta.content or "", end="")

Shape the output

The usual sampling controls all work: temperature, top_p, top_k, min_p, max_tokens, stop, seed, and the frequency_penalty / presence_penalty / repeat_penalty repetition controls. Lower temperature for predictable, factual answers; raise it for more variety. At temperature: 0 the model always picks its most likely next token, the most focused and repeatable setting. Penalties default to off, so output is unchanged unless you set them.

Even so, output isn't bit-for-bit reproducible: the same request can vary slightly between runs, because generation runs on GPU hardware where tiny numerical differences occasionally change a token. If you're running a high-recall job — extracting every defect from a document, say, where a miss is costly — send the request twice and take the union of the results. A second pass reliably catches the few items a single pass happens to miss:

# High-recall extraction: two passes, then union the findings
def extract(document):
    response = client.chat.completions.create(
        model="qwen3.6:27b",
        temperature=0,
        messages=[
            {"role": "system", "content": "Extract every defect. Reply as JSON."},
            {"role": "user", "content": document},
        ],
    )
    return parse(response.choices[0].message.content)

runs = [extract(document) for _ in range(2)]
findings = {item["id"]: item for run in runs for item in run}
# 'findings' now holds every item either pass found

Go further

Looking for every field, response shape, and header? See the Chat completions API reference.