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 role — system (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.

Where a model's authors publish recommended sampling settings, Pendra uses them for any parameter you don't set. Anything you send overrides them. You can see each model's settings as sampling_defaults in the models list. Models without a recommendation use a temperature of 1.0, with the penalties off.

Because those recommendations can be updated, set the parameters you care about explicitly when you need output that stays consistent over time — for example in an evaluation or a regression test:

# Pin the sampling settings so results don't shift if the
# model's recommended defaults change
response = client.chat.completions.create(
    model="qwen3.6:27b",
    temperature=0.2,
    top_p=0.9,
    seed=42,
    messages=[{"role": "user", "content": "Summarise this ticket in one line."}],
)

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.