---
title: Pixel mode
summary: Render text as an image the model reads for fewer tokens. Which models, and how to check it paid off.
canonical: https://docs.caveman.so/docs/proxy/pixel
layer: proxy
license: BSL-1.1
capability: engine
updated: 2026-09-16T21:32:40-07:00
basis: inferred
---

# Pixel mode

> Render text as an image the model reads for fewer tokens. Which models, and how to check it paid off.
Pixel mode renders text into a PNG from an embedded glyph atlas and sends the image where the text would
have gone, because a model that reads images well charges by image patch rather than by token. It is a
lossy transform like any other compressor, so the original request is stored under a recovery handle first,
and it runs only for models on an allowlist.

Before wiring anything: check whether the model you use is on that list, because for every other model
the transform is skipped and nothing changes.

## Price a request without sending it

`pixel simulate` reads a request body from disk, runs the real transform, and prints what it would do. It
sniffs the shape: `input` is an OpenAI Responses request, `contents` is Gemini, `messages` is Anthropic
Messages.

```bash
curl -O https://docs.caveman.so/examples/engine/request.json
caveman-engine pixel simulate request.json
```

```json
{
  "Compressed": true,
  "Reason": "",
  "OrigChars": 7869,
  "ImageCount": 1,
  "ImageBytes": 24550,
  "ImagePixels": 272738,
  "TextTokensEstimate": 4231,
  "ImageTokensEstimate": 1013,
  "StaticChars": 7869,
  "DynamicChars": 0,
  "DroppedChars": 0
}
```

A payload under the threshold is left alone and says so:

```json
{ "Compressed": false, "Reason": "below_min_chars (0 < 2000)", "OrigChars": 0, "ImageCount": 0 }
```

That `<` is a `<`, escaped by the Go JSON encoder. Captured from a caveman-engine build dated
2026-08-24 on a request whose system prompt is 7,871 characters, trimmed to the fields this page names;
`caveman setup --install` today pins bin-v1.1.7, so digits may move. `--model` overrides or fills the model
in the body.

## Which models

The allowlist defaults to `claude-fable-5` and `gpt-5.6`. A model matches when it equals an entry or starts
with that entry plus a hyphen, after any `[…]` variant tag is stripped, so dated and vendor-prefixed names
resolve to their base.

`CAVE_PIXEL_MODELS` replaces the list with a comma separated one of your own. Setting it to `0`, `false`,
`no`, `off` or `none` empties the list, which turns the transform off everywhere.

The proxy checks the allowlist before it does anything else in `pixel` mode. Off the list, the request goes
upstream as the client sent it. On the list, the transform runs, the original request body is stored under a
CCR handle, and the response carries `pixel-render` in `x-cave-optimization`. A failed recovery write leaves
the original bytes alone.

## What gets rendered

In `pixel` mode the proxy renders the live zone, the part of the conversation the provider has not cached.
See [Prompt caching](/docs/proxy/caching#where-the-frozen-prefix-ends) for how that boundary is found.
Three size thresholds decide what is worth an image: 2,000 characters for a text block, 6,000 for a tool
result, and 6,000 for an injected reminder.

Past the threshold, a profitability gate prices the image against the text. It counts the patch tokens the
rendered page would cost, applies a 1.10 safety margin, and compares that against the text at two
characters per token for a cached slab. It also prices the cache burn on both sides, using Anthropic's 1.25
write rate and 0.10 read rate, so switching a warm text prefix to an image is charged for the rebuild it
causes. The transform runs when the image side comes out lower.

## Render a file yourself

```bash
curl -O https://docs.caveman.so/examples/engine/SKILL.md
caveman-engine pixel render SKILL.md
```

```json
{"width":1565,"height":202,"charsRendered":12352,"droppedChars":0,"estTokens":493,"density":"balanced"}
{"summary":true,"pages":1,"textEstTokens":3235,"imageEstTokens":493,"density":"balanced"}
```

Pages are written next to the input as `SKILL.md.px1.png`, `SKILL.md.px2.png` and so on, one JSON line per
page and a summary line last. The two numbers that matter are on the summary: `textEstTokens` is the
offline `o200k_base` count of the input, `imageEstTokens` is the patch estimate with its margin. The render
paid off when the second is smaller than the first, and `droppedChars` is 0.

`--density` picks the cell geometry. `conservative` is the standard mono grid at 28,080 characters per
page, `balanced` is a denser zebra grid and the default, `max` stacks two red and blue text layers into one
image. The same file at conservative geometry:

```json
{"width":1568,"height":328,"charsRendered":12360,"droppedChars":0,"estTokens":740,"density":"conservative"}
{"summary":true,"pages":1,"textEstTokens":3235,"imageEstTokens":740,"density":"conservative"}
```

This command draws exactly the level you ask for, whatever model you plan to show the image to. `max`
stacks two layers once a page has enough text to fill one, and the report then carries `"layers":2`; this
12 KB file fits in a single layer, so max and balanced draw the same page.

The model profile decides the geometry on the proxy path instead. `claude-fable-5`, `claude-mythos-5`,
`claude-sonnet-5` and `claude-opus` from 4.8 onward get the higher resolution canvas at whichever level is
asked for. `gpt-5.6` is recognised too and gets the level on the standard canvas. Any other model, and any
model the table does not know, resolves to conservative standard geometry with one layer at every level.

An unrecognised density value is a usage error rather than a silent downgrade:

```bash
caveman-engine pixel render --density ultra SKILL.md
```

```text
--density must be one of conservative|balanced|max (got "ultra")
```

## Pixel-compressing a skill

`caveman convert` runs the same renderer over installed agent skills, replacing each `SKILL.md` body with
image pages and a short stub that tells the agent to read them. `--dry-run` reports the arithmetic and
writes nothing:

```bash
mkdir -p skills/release-checks
curl -o skills/release-checks/SKILL.md https://docs.caveman.so/examples/engine/SKILL.md
caveman convert --dir ./skills --dry-run
```

```text
release-checks: 3217 → 613 est tokens (−81% inferred) (dry-run)
total: 3217 → 613 est tokens (−81% inferred)
```

Drop `--dry-run` and three files are left in the skill directory:

```bash
caveman convert --dir ./skills
```

```text
release-checks: 3217 → 613 est tokens (−81% inferred)
total: 3217 → 613 est tokens (−81% inferred)
```

```bash
ls skills/release-checks
```

```text
SKILL.md
SKILL.orig.md
SKILL.px1.png
```

`SKILL.md` keeps its frontmatter, because skill discovery reads it as text, and its body becomes the stub:

```markdown
<!-- caveman-pixel v1 sha256:b824175f… -->
This skill's full instructions are pixel-compressed into image pages to save
tokens. Read (view) these image files NOW, in order, and follow their contents
as this skill's complete instructions:

1. /path/to/skills/release-checks/SKILL.px1.png

Plain-text original: SKILL.orig.md in this directory
(restore with `caveman convert --revert`).
```

`SKILL.orig.md` is written before anything else is touched, so a crash part way through still leaves a
complete original to restore from.

Both figures on that line come from a render of the body alone, with the frontmatter stripped, and with
`--dense` on, which is the packing that makes an image beat the text it replaces. That render reports 3,217
text tokens and 493 image tokens for this file. The after figure adds a flat 120 estimated tokens for the
stub, giving 613, and that sum is what the not-smaller check compares against 3,217.

Undo it:

```bash
caveman convert --dir ./skills --revert
```

```text
release-checks: reverted
```

The revert restores `SKILL.md` from `SKILL.orig.md`, deletes the page images and removes the backup. Run
without `--dir`, `caveman convert` walks the skill directories of every agent profile that has one, and
`--agent <id>` narrows it to one.

Conversion is skipped with the reason printed. These are the reasons, verbatim:

```text
no frontmatter — discovery needs it as text
render dropped chars — kept as text
not smaller — kept as text
already converted
already converted but SKILL.orig.md is missing
stale SKILL.orig.md exists — use --force to overwrite
caveman-engine not found — kept as text; run `caveman setup`
unknown skill surface format
render failed (exit N)
```

The stale one is the case that surprises people. A leftover `SKILL.orig.md` beside a `SKILL.md` that carries
no pixel marker means an earlier revert was interrupted, so the tool leaves the only plain-text copy it can
see alone:

```bash
caveman convert --dir ./skills
```

```text
release-checks: skipped — stale SKILL.orig.md exists — use --force to overwrite
```

The repository's own measurement on the caveman skill is 1,069 estimated tokens to 415, a 61 percent cut.
That figure is `inferred`, and [Numbers and limits](/docs/counting#vocabulary) says what the word covers.

## Limits

- Reads it back: Only the allowlisted models. Everything else gets the text.
- Recovery: The original request is stored under a CCR handle before any image is sent.
- Blocked by size: Under 2,000 characters for a text block, or 6,000 for a tool result, nothing is rendered.
- Blocked by price: The gate reports `not_profitable` and the text goes through.
- WASM: The pixel package is never compiled into the browser build, because the atlases are about 4 MB.

Pixel rendering is never chosen by content detection. It is a request-level mode, set by `mode: pixel` in
`caveman.yaml`, and the engine's content compressors are a separate path. See [Modes](/docs/proxy/modes).

The renderer is a port of [pxpipe](https://github.com/teamchong/pxpipe), MIT licensed, with PNG encoding
moved to the Go standard library and token estimates moved to the engine's offline counter.
