Metadata-Version: 2.5
Name: offrec
Version: 0.5.0
Summary: Official Python client for offrec: zero-retention inference for frontier models, with a signed receipt for every call.
Project-URL: Homepage, https://offrec.ai
Project-URL: Documentation, https://docs.offrec.ai
Project-URL: Console, https://console.offrec.ai
Author-email: offrec <info@offrec.ai>
Requires-Python: >=3.9
Requires-Dist: httpx<0.29,>=0.24
Provides-Extra: private
Requires-Dist: numpy>=1.23; extra == 'private'
Description-Content-Type: text/markdown

# offrec

Official Python client for **offrec**: zero-retention inference for frontier models.
AI, off the record.

- **Frontier models, one key.** Claude, Gemini, Gemma, DeepSeek, Kimi, Qwen and GLM
  through `https://api.offrec.ai/v1`, priced per model.
- **Zero retention.** offrec never stores or logs your prompts or the answers. The
  models run on managed providers under zero-retention terms.
- **A receipt for every call.** Signed with Ed25519 and checkable against a public
  key.

Docs: [docs.offrec.ai](https://docs.offrec.ai) · Console:
[console.offrec.ai](https://console.offrec.ai) · R package: `install.packages("pnotp")`

> **Formerly `pnotp`.** `pip install offrec` replaces `pip install pnotp`, with the same
> API: change `import pnotp` to `import offrec`. Nothing else has to change:
> `pnotp_sk_` keys, `PNOTP_API_KEY` / `PNOTP_BASE_URL` and `except PnotpError` keep
> working, and `pnotp` 0.5.0 on PyPI is an alias of this package.

## Install

```bash
pip install offrec
```

Requires Python 3.9 or later. The only hard dependency is `httpx`.

## Authenticate

Mint an account key at **[console.offrec.ai/api-keys](https://console.offrec.ai/api-keys)**
(it starts with `offrec_sk_` and is shown once) and add credits on the console's
Billing page. A new account starts with none; a call on an empty balance raises
`offrec.InsufficientCreditsError` (402).

```bash
export OFFREC_API_KEY="offrec_sk_..."
```

```python
import offrec
px = offrec.Client()                       # reads OFFREC_API_KEY
# or: px = offrec.Client(api_key="offrec_sk_...")
```

## Your first call

```python
out = px.chat("Summarize this clause in one sentence: ...", model="gemini-3.1-flash-lite")

out["completion"]                         # the answer
out["usage"]                              # {"input_tokens": ..., "output_tokens": ...}
out["receipt"]["zero_retention"]          # True
out["receipt"]["served_by"]               # who ran the call, and who governs retention there
```

Change `model` to use any model on the list. A few starting points:

| `model` | Use it for | $ in / out per 1M tokens |
|---|---|---|
| `gemini-3.1-flash-lite` | The default: fast, reads images, tool calling | $0.25 / $1.50 |
| `claude-haiku-5-5` | The lowest price on the list; 1M context ($0.50 / $2.50 over 100K tokens of prompt) | $0.10 / $0.50 |
| `gemma-4-26b` | Fast and multimodal at a low price | $0.15 / $0.60 |
| `deepseek-v4.1-flash` | Agents and 1M-token context at a low price | $0.30 / $1.20 |
| `claude-sonnet-5-5` | Coding, agents and knowledge work, 1M context | $2.00 / $10.00 |
| `claude-opus-5-5` | Frontier coding and long-horizon agent work | $4.00 / $20.00 |

The live list, with every price and context window, is public:
`GET https://api.offrec.ai/v1/llm/pricing`. Gemini and Gemma run on Google Cloud
Vertex AI, Claude on Amazon Bedrock, and DeepSeek, Kimi, Qwen and GLM on
Fireworks AI.

## Tools, images and documents

```python
out = px.chat("Book a 3pm slot with Dr. Reyes", tools=[{"type": "function", "function": {...}}])
out.get("tool_calls")                     # [{"id", "name", "arguments": {...}}]

px.chat("What is in this diagram?", image="diagram.png")       # on a model with vision
px.chat("Summarize this contract", file="contract.pdf")        # parsed to text
```

A model sees images natively when its `GET /v1/llm/pricing` entry says
`vision: true`. Audio is not enabled today (a clean 422).

## The receipt

Every call returns a receipt next to the answer: the retention basis, the model,
the tokens, the cost and who served it. It carries no prompt and no answer. offrec
keeps no copy of the call, so store the receipts you need.

The receipt is signed with Ed25519. The public key is at
`GET https://api.offrec.ai/v1/llm/attestation`, and the verification recipe is on
[docs.offrec.ai/receipts](https://docs.offrec.ai/receipts).

## Already on the OpenAI SDK?

The API speaks OpenAI's Chat Completions. Change two lines and keep your code:

```python
from openai import OpenAI

client = OpenAI(base_url="https://api.offrec.ai/v1", api_key="offrec_sk_...")
resp = client.chat.completions.create(model="claude-sonnet-5-5", messages=[...], tools=[...])
resp.receipt                              # the signed receipt
```

Streaming, `tool_choice`, the Vercel AI SDK and LangChain work the same way. See
[docs.offrec.ai/migrating](https://docs.offrec.ai/migrating).

## Laya: typed decisions

Laya answers typed questions (choice, score, yes/no) about a ticket, an email or a
conversation with calibrated probabilities, and writes no text.

```python
result = px.decide(
    "Checkout has been down for an hour and we're losing sales. Fix it or we cancel.",
    {
        "queue": {"type": "choice", "instructions": "Which team owns this ticket?",
                  "criteria": {"infrastructure": "outages, downtime, errors",
                               "billing": "refunds, invoices, charges"}},
        "churn_risk": {"type": "noul", "instructions": "Does the customer threaten to cancel?"},
    },
)
result["answers"]["queue"]["choice"]      # "infrastructure"
```

Guide: [docs.offrec.ai/laya](https://docs.offrec.ai/laya).

## Hosted agents

A hosted agent runs a tool-calling loop with a zero-retention model as its brain.
A data-compliant run keeps no question, no intermediate reasoning and no loop
state: only metered metadata and the encrypted final answer, held for 24 hours.

```python
a = px.agents.create("Advisor")
px.agents.publish(a["id"], model="gemini-3.1-flash-lite", tools=["programs_db"])
run = px.agents.run(a["id"], "I love AI and maths. Which programs fit?", wait=True)
print(run.final_answer())
```

## SDK agents (bring your own brain)

`offrec.Agent` is a bounded, observable tool-calling loop that runs in your
process. The brain can be your own provider key (any OpenAI-style
`/chat/completions` with tool calling) or a text model you deployed on offrec. With
your own key, tokens go to that provider, not through offrec's zero-retention layer.

```python
import offrec

agent = offrec.Agent("gemini:gemini-2.5-pro",          # reads GEMINI_API_KEY
                    system="You are a careful research assistant.")

@agent.tool
def search_pubmed(query: str, max_results: int = 5) -> list[dict]:
    "Search PubMed and return the top hits."          # docstring -> tool description
    return my_client.search(query, k=max_results)     # JSON Schema is derived from the signature

run = agent.run("Recent trials on pediatric pneumonia from chest X-rays?")
print(run.output, run.stop_reason)                    # final answer + "stop" | "max_steps"

for event in agent.stream("..."):                     # live UI events
    ...   # TextDelta / ToolCallStarted / ToolCallFinished / StepBoundary / Done
```

The loop is bounded: `max_steps`, per-tool `tool_timeout`, `max_tool_retries` (a
schema-invalid or timed-out call is re-asked without spending the step budget),
and `on_tool_error` (`"return"` feeds the error back to the brain, or `"raise"`).
Pass `response_format=MyDataclass` for a structured final answer.

## Neural networks (an extra)

Training and deploying your own models is a side feature that still works end to
end:

```python
project = px.projects.create(name="Chest X-ray", task_type="binary_image_classification")
model = px.models.create(
    project["id"], name="cnn-v1",
    architecture=offrec.architectures.image_classifier(num_classes=2),
)

# dataset = a .zip of class-named folders (image) or a .csv (tabular)
job = px.train(model.id, dataset="train.zip", epochs=5)
job.wait(on_progress=lambda j: print(j.status))      # job.pause() / resume() / cancel()

dep = px.deployments.upload(project["id"], name="prod", checkpoint="model.pt", compliant=True)
print(px.predict(dep["deployment"]["id"], "xray.jpg"))
```

CPU training is free; GPU training bills $1.00 per hour on an NVIDIA L4, per second
of compute. `compliant=True` makes an endpoint data-compliant: the input is deleted
before the response returns, error text stays out of the stats log, and each
response carries a `compliance` block. External consumers can call an endpoint with
a per-deployment `pnp_...` key (`px.deployments.create_key(...)`). Guide:
[docs.offrec.ai/neural-networks](https://docs.offrec.ai/neural-networks).

## Help and Toby

The SDK documents itself, offline, with no key and no network:

```python
offrec.help()                       # overview + the list of topics
offrec.help("quickstart")           # a topic page
offrec.help.search("stream a chat") # search topics + the live API reference
offrec.help(offrec.Client.chat)      # any symbol's real signature + docstring
offrec.help.errors                  # the exception catalog
```

**Toby**, the hosted assistant, answers in plain English (needs your key and a
positive balance; it falls back to local doc search otherwise, so it never raises):

```python
print(offrec.help.toby("Which model should I use for long contracts?"))
```

## Surface

| Area | Calls |
|---|---|
| LLM | `chat(text_or_messages, model=, tools=, image=, file=, ...)` returns `{completion, usage, model, receipt}` |
| Laya | `decide(state, questions, model="laya", min_confidence=)` |
| Hosted agents | `agents.create/get/list/publish(model="gemini-3.1-flash-lite", tools=, ...)/run(..., wait=)` · `run.final_answer/steps/progress` |
| SDK agents | `Agent(model, tools=, system=, ...)` · `agent.run/stream` · `@agent.tool` / `@offrec.tool` · `offrec.endpoints.*` · `offrec.model` |
| Credits | `credits.balance()` |
| API keys | `api_keys.create/list/revoke` |
| Projects and models | `projects.list/create/get` · `models.create/list/get/add_version/versions` |
| Training | `train(...)` returns a `TrainingJob` · `job.wait/progress/pause/resume/cancel` · `jobs.list/get` |
| Deployments | `deployments.upload/list/get/predict/stats/pause/resume/delete` · `create_key/list_keys/revoke_key` |
| Help | `help()` / `help("topic")` / `help(symbol)` · `help.search/topics/errors` · `help.toby(...)` · `help.as_tool()` |

Every error subclasses `offrec.OffrecError` (also importable as `PnotpError`).
API errors raise `offrec.APIError` subclasses: `AuthError` (401/403), `NotFoundError`
(404), `InsufficientCreditsError` (402). Agent-loop failures raise
`offrec.AgentError` subclasses: `MaxStepsExceeded`, `ToolError`, `ModelEndpointError`.

---

The legacy `pnotp_api` package (a standalone PathoVision wrapper, unrelated to the
client above) no longer ships with this package. It stays in `pnotp` 0.5.0:
`pip install "pnotp[server]"`.
