# Encrypted inference

> Use end-to-end encrypted :e2ee models from Chat, coding agents or the Tinfoil SDK, and understand what each provider protects.

Source: https://0.0.0.0:3000/docs/inference/encrypted
Documentation index: https://0.0.0.0:3000/docs/llms.txt

Encrypted models end in `:e2ee`. Your prompt and the response are encrypted on your device, and Numes and the provider operator relay data they can't read. Two providers offer encrypted models, and their guarantees differ:

- **Tinfoil** runs the model in a hardware enclave that your client verifies before it sends anything. If verification fails, the request stops.
- **Venice** encrypts message text to a key that Venice publishes. It's a weaker guarantee than Tinfoil's; read [Venice security boundary](#venice-security-boundary) before you rely on it.

An encrypted request never falls back to ordinary inference.

## Find encrypted models

Encrypted models show a lock after their name in the dashboard, on public model pages and in this documentation. To list only encrypted models, choose the **Encrypted** filter in the Chat model picker, or set **Privacy** to **Encrypted** in **Models**. An encrypted model's page summarizes what stays protected, and an ordinary model's page links its encrypted variant when one exists.

An encrypted model keeps the name of the regular model; the `:e2ee` ID tells them apart. [Organization models](/docs/account/organizations) can combine both qualifiers, as in `author/model:e2ee:org-team`. Use an ID from the catalogue rather than adding `:e2ee` to an ordinary model.

`POST /chat/completions` doesn't accept `:e2ee` IDs, and sending ordinary JSON never encrypts it. Use [Chat](#use-encrypted-models-in-chat), the [local bridge](#use-encrypted-models-from-coding-agents-and-other-clients) or the [Tinfoil SDK](#direct-sdk-setup).

## Privacy boundary

| | Tinfoil | Venice |
| --- | --- | --- |
| Encrypted to | A Tinfoil enclave whose attestation your client verifies | A key published by Venice |
| Protected | The whole request and response: messages, reasoning, tool calls and their arguments | System, user and assistant message text, and response text and reasoning |
| Requests | Text chat with reasoning, structured output and function tools that your client runs | Text chat only |
| When verification fails | The request stops | Most checks are reported in [status headers](#verification-status) and the request continues |

For both providers:

- **What stays visible:** the model ID, the selected endpoint, your account, token counts, duration and the size of the encrypted data. These are needed to route and bill the request. With Venice, message roles and order and generation settings such as `temperature` are also visible.
- **What you must trust:** your device, browser and extensions, and the client software. In Chat, encryption runs in JavaScript served by Numes. Chat history and coding-agent files stay readable on your device.
- **What isn't available:** image, file and audio input, [hosted tools](/docs/features/hosted-tools), embeddings, image and audio generation, provider routing preferences and [sticky sessions](/docs/features/sessions). Function tools that your client runs can send data to whatever they call.

### Request policy

Encrypted requests can't be inspected, so an account's [request policy](/docs/privacy/request-policy) can't redact them, add context, restore output or retain content. Encrypted models skip the policy by default; the policy keeps applying to regular models. Owners and administrators can choose **Block E2EE models** in **Security** to reject encrypted requests while the policy is on. Authentication, organization access, key limits, capacity and spending limits still apply.

### Venice security boundary

Venice offers a weaker guarantee than Tinfoil:

- **You trust Venice's key.** Your client encrypts to the key Venice publishes over HTTPS. It also checks Intel CPU and NVIDIA GPU evidence where available, but that evidence doesn't prove which software handles your request, and missing or rejected evidence doesn't stop the request.
- **Replies can be exposed to an active attacker.** Your client sends the key Venice encrypts replies to in a request header that nothing signs, so an intermediary that tampers with traffic could substitute its own key and read the replies.
- **The receipt doesn't prove your transcript.** Venice signs a receipt, and an invalid signature fails the request, but a valid one doesn't prove that the conversation Venice processed is the one you sent and received.

In practice, Venice keeps your messages out of logs and other passive observation along the relay path, but it doesn't protect against an attacker who actively modifies traffic, and you rely on Venice itself. Choose Tinfoil when you need confidentiality you can verify independently.

Venice accepts system, user and assistant messages with text content, `stream`, `max_tokens` or `max_completion_tokens`, `temperature`, `top_p`, `seed`, `frequency_penalty` and `presence_penalty`. Everything else is rejected before the request is sent, including tools, developer and tool messages, images, files and audio, `response_format`, reasoning controls, stop sequences and identifiers such as `user`. Coding agents can't use Venice for workflows that need function tools.

Use Venice models in Chat or through the local bridge; the SDK setup below covers Tinfoil only. NVIDIA's evidence service doesn't accept requests from browsers, so Chat fetches that evidence through Numes and verifies it in your browser; the relay can't make failed evidence pass.

### Verification status

For Venice responses, the local bridge returns these headers. They report evidence checks only: `verified` doesn't mean the workload was independently approved, and the headers don't affect billing.

| Header | Values | Meaning |
| --- | --- | --- |
| `x-encryption-security-policy` | `provider-managed` | The Venice trust model described above. |
| `x-encryption-receipt-signature` | `verified`, `unavailable` | Whether Venice's signed receipt was received and its signature checked. An invalid signature fails the request. |
| `x-encryption-transcript` | `verified`, `unverifiable`, `unavailable` | Whether the receipt proves this conversation. `unverifiable` means the signature is valid but doesn't cover what you sent and received. |
| `x-encryption-cpu-hardware` | `verified`, `unverified` | Intel CPU evidence. |
| `x-encryption-gpu-hardware` | `verified`, `unverified` | NVIDIA GPU evidence. |

Tinfoil responses don't carry these headers: a Tinfoil response only arrives after the enclave's attestation has been verified.

## Use encrypted models in Chat

Choose a model with the lock in Chat. Encryption and decryption happen in your browser, and conversations stay in browser storage. The conversation shows the lock in the history list while it continues with an encrypted model.

Switching to a regular model, or regenerating or editing with one, asks for confirmation before protected history is sent unencrypted. Returning to an encrypted model withdraws that approval, and a reload asks again. If a request can't be sent, Chat tells you whether the request is unsupported, verification failed or your account's policy blocks encrypted models, and keeps your draft so you can retry.

## Use encrypted models from coding agents and other clients

After you sign in with the `numes` command, run `numes run opencode`. When encrypted models are available, the OpenCode model picker shows two providers: **Numes** for regular models and **Numes E2EE**, which goes through a local bridge that encrypts on your machine. OpenCode starts on a regular model; select an encrypted model in the picker to use it. See [Coding agents](/docs/integrations/coding-agents) for setup.

To continue an encrypted conversation with a regular model, select that model and run `/allow-unencrypted-history`. Until you do, protected history isn't sent to regular models, including in resumed sessions, compaction and sub-agents. In OpenCode 2, selecting an encrypted model also protects the first prompt before title generation; a regular title-model override is blocked. This guard relies on OpenCode's request hooks and is tested with OpenCode 1.18.22 and 2.1.4; other versions show a warning. It doesn't protect against malicious plugins or modified client software.

For any other OpenAI-compatible client, run the bridge yourself:

```bash
export NUMES_E2EE_BRIDGE_KEY="<a private token of 32-256 characters>"
numes bridge
```

Point your client at the printed loopback `/v1/` URL, use the bridge key as its API key, and choose an available `:e2ee` model ID. The bridge key is separate from your inference key. Press Ctrl-C to stop the bridge. The bridge's port and limits live in the `[encrypted_client]` section of the CLI settings file. The bridge can't control what a third-party app sends to its other providers.

## Direct SDK setup

You can call Tinfoil models directly with Tinfoil's official SDK. JavaScript SDK 1.2.2 and Python SDK 0.14.0 are tested.

1. [Resolve the model](/docs/api-reference/encrypted-inference/resolve-encrypted-model) with your inference key. Resolution sends no prompt and isn't charged.
2. Check that `transport` is `tinfoil-ehbp`, `protocol` is `ehbp-v1` and `security_policy` is `attested-enclave`.
3. Send the request through the SDK with the returned `native_model`, and pass the returned model and endpoint in the headers shown below. Availability, access and balance are checked again when you send.

```typescript
import { TinfoilAI } from "tinfoil";

const base = "https://api.numes.ai/api/v1/"; // trailing slash for relative URLs
const key = process.env.NUMES_API_KEY!;
const selectedModel = process.env.INFERENCE_MODEL!;
const response = await fetch(new URL("e2ee/resolve", base), {
  method: "POST",
  headers: {
    Authorization: `Bearer ${key}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ model: selectedModel }),
  redirect: "error",
});
if (!response.ok) throw new Error("Encrypted model unavailable");
const route = await response.json();
if (
  route.transport !== "tinfoil-ehbp" ||
  route.protocol !== "ehbp-v1" ||
  route.security_policy !== "attested-enclave"
)
  throw new Error("Unsupported encryption policy");
const client = new TinfoilAI({
  apiKey: key,
  baseURL: new URL("e2ee/", base).toString(),
  transport: "ehbp",
  maxRetries: 0,
  userCacheSecret: crypto.randomUUID(),
});
const reply = await client.chat.completions.create(
  {
    model: route.native_model,
    messages: [{ role: "user", content: "Hello" }],
    max_tokens: 192,
  },
  {
    headers: {
      "X-Inference-Model": route.model,
      "X-Inference-Offer-Id": route.offer_id,
      "X-Inference-Offer-Revision-Id": route.offer_revision_id,
      "X-Inference-Native-Model": route.native_model,
      "X-Inference-Profile": route.transport,
    },
  },
);
console.log(reply.choices[0]?.message.content);
```

```python
import os
import uuid
import httpx
from tinfoil import TinfoilAI

base = "https://api.numes.ai/api/v1/"  # trailing slash for relative paths
key = os.environ["NUMES_API_KEY"]
with httpx.Client(timeout=20, follow_redirects=False) as resolver:
    response = resolver.post(base + "e2ee/resolve",
        headers={"Authorization": "Bearer " + key},
        json={"model": os.environ["INFERENCE_MODEL"]})
    response.raise_for_status()
    route = response.json()
if (route["transport"], route["protocol"], route["security_policy"]) != (
        "tinfoil-ehbp", "ehbp-v1", "attested-enclave"):
    raise ValueError("Unsupported encryption policy")
client = TinfoilAI(api_key=key, base_url=base + "e2ee/", transport="ehbp",
    attestation_bundle_url="https://atc.tinfoil.sh", timeout=60,
    user_cache_secret=str(uuid.uuid4()))
with client.client.with_options(max_retries=0) as api:
    reply = api.chat.completions.create(
        model=route["native_model"],
        messages=[{"role": "user", "content": "Hello"}], max_tokens=192,
        extra_headers={
            "X-Inference-Model": route["model"],
            "X-Inference-Offer-Id": route["offer_id"],
            "X-Inference-Offer-Revision-Id": route["offer_revision_id"],
            "X-Inference-Native-Model": route["native_model"],
            "X-Inference-Profile": route["transport"],
        })
    print(reply.choices[0].message.content)
```

- **Attestation.** The SDK verifies the enclave itself, using Tinfoil's attestation service directly; your client needs access to it. Numes doesn't proxy attestation.
- **Cache namespace.** `userCacheSecret` (`user_cache_secret` in Python) gives your requests a private prompt-cache namespace inside the encrypted body. Reuse one value across a conversation so repeated prefixes can hit the provider's cache.
- **Streaming.** Treat a stream as complete only when the SDK reports the end of the encrypted body, not on an early `[DONE]`.
- **Retries.** Turn off generic SDK retries, as the examples do. The dashboard and CLI can resolve and encrypt one fresh attempt when the gateway explicitly returns `503 inference_not_dispatched`, confirming that execution never started. Direct SDK clients may do the same with a new idempotency key and the same encryption policy. Other errors and interrupted bodies do not authorize automatic retry. Never retry with a regular model automatically.

## Billing and limits

- **What you pay for.** Encrypted requests are billed on the provider's reported prompt and completion tokens, reasoning included, at the endpoint's rates. Cache discounts apply only when the provider reports cache usage.
- **When billing starts.** Once encrypted output starts reaching you, the request is billed for the provider's final usage, even if you cancel or disconnect. The provider finishes the response, and nobody can count the tokens inside encrypted data, so the charge can include tokens generated after you disconnect. A request that delivers no output isn't charged.
- **Don't retry incomplete responses automatically.** An interrupted encrypted response may already be charged, and a retry is a new, separately billed request.
- **Holds and rate limits.** Your balance and key limits must cover a temporary hold sized for the model's full context window, and token rate limits count that window. You're charged only for actual usage. See [Credits](/docs/account/credits).
- **Where to see the charge.** Look up the settled cost in [Activity](/docs/account/activity) or with [`GET /generation`](/docs/account/usage-accounting) using the `X-Generation-Id` response header. SDK responses contain the provider's own metadata; ignore any cost estimate in it.

Speed and quality rankings don't include encrypted requests.

## Next steps

:::cards
- {shield} [Request policy](/docs/privacy/request-policy) How account policies treat encrypted models.
- {terminal} [Coding agents](/docs/integrations/coding-agents) Set up `numes run` and choose models.
- {receipt} [Usage accounting](/docs/account/usage-accounting) Read usage and cost for every request.
- {boxes} [Models](/docs/models) Browse encrypted models in the catalogue.
:::
