# Using Small Paper with opencode

[opencode](https://opencode.ai) reaches Small Paper through the AI SDK's OpenAI-compatible
provider: a config block. Works with opencode 1 and 2. The installer adds a plugin that refreshes
the otherwise frozen model list at startup.

## Install

```sh
curl -fsSL https://smallpaper.io/install.sh | bash
```

On Windows, in PowerShell (5.1 or 7):

```powershell
irm https://smallpaper.io/install.ps1 | iex
```

It asks whether to configure opencode globally or for one project, then takes your account
number or pairs through the browser. Pairing creates a **separate key for that machine**, so a
laptop is revocable without changing your account number. Non-interactive:

```sh
curl -fsSL https://smallpaper.io/install.sh | bash -s -- --global --key 41920837556120947381
curl -fsSL https://smallpaper.io/install.sh | bash -s -- --local --pair
curl -fsSL https://smallpaper.io/install.sh | bash -s -- --global --pair --dry-run   # print, write nothing
```

The PowerShell installer takes the same flags as `-Global`, `-Local`, `-Key`, `-Pair` and
`-DryRun`; `iex` cannot pass them, so use the script-block form:

```powershell
& ([scriptblock]::Create((irm https://smallpaper.io/install.ps1))) -Global -Key 41920837556120947381
& ([scriptblock]::Create((irm https://smallpaper.io/install.ps1))) -Local -Pair
```

The installer **merges** into an existing config and keeps a timestamped backup. It only
ever writes Small Paper's provider entry, and it refuses to touch a config it cannot parse. An
opencode 2 style `providers.smallpaper` entry is replaced, because opencode 2 would prefer it to the
one the installer writes.

Then run `/models` inside opencode and pick a Small Paper model.

## Keeping the model list current

Two files get written:

| | |
|---|---|
| `opencode.json` | the provider block, including a snapshot of the models |
| `plugin/smallpaper.js` | refreshes that model list at startup |

The plugin loads from that directory: nothing to configure, no duplicate on re-run. It replaces
only the model list from `https://api.smallpaper.io/docs/opencode.json`, never your key; unreachable, it gives up
within ~2 s.

Delete `plugin/smallpaper.js` if you would rather pin the model list. Re-run the installer when it
needs updating.

The plugin needs opencode 1.3.4 or newer; on anything older the installer
skips it. On opencode 2 it adds models we start serving after you installed, but it cannot
retire or reprice the ones already in your `opencode.json`, because opencode 2 applies the file's
own entries last. Re-run the installer for those.

**Moved to opencode 2?** Re-run the installer. A plugin from an earlier install only speaks
opencode 1's plugin API, so opencode 2 lists it as failed under `/plugins`.

A 404 naming a model you thought you had means your config predates a rename; the error says
what replaced it, and re-running the installer repoints you.

## Config, by hand

The generated block with current models, limits and prices is always at
**[https://api.smallpaper.io/docs/opencode.json](https://api.smallpaper.io/docs/opencode.json)**.

> **Do not `curl -o` over your global config.** `~/.config/opencode/opencode.json` holds
> your other providers, agents, keybinds and MCP servers, and opencode merges settings
> across levels rather than replacing them. Redirecting the generated file over it
> deletes all of that. Paste the `provider` block into your existing file instead, or let
> the installer merge it.

```json
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "smallpaper": {
      "models": {
        "deepseek-v4.1-flash": {
          "attachment": true,
          "cost": {
            "cache_read": 0.0015,
            "input": 0.06,
            "output": 0.24
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 128000
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "DeepSeek V4.1 Flash 1017",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "deepseek-v4.1-flash-1017-abliterated": {
          "attachment": true,
          "cost": {
            "cache_read": 0.035,
            "input": 0.5,
            "output": 1.9
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1000000,
            "output": 32768
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "DeepSeek V4.1 Flash 1017 [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "gemini-4-argon": {
          "attachment": true,
          "cost": {
            "cache_read": 0.03,
            "input": 0.48,
            "output": 2.3
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 65536
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Gemini 4 Argon",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "gemini-4-carbon": {
          "attachment": true,
          "cost": {
            "cache_read": 0.03,
            "input": 0.45,
            "output": 2.1
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 65536
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Gemini 4 Carbon",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "glm-5.3-abliterated": {
          "attachment": false,
          "cost": {
            "cache_read": 0.75,
            "input": 4.725,
            "output": 7.5
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1000000,
            "output": 32768
          },
          "modalities": {
            "input": [
              "text"
            ],
            "output": [
              "text"
            ]
          },
          "name": "GLM 5.3 [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "glm-5.3-cyber": {
          "attachment": false,
          "cost": {
            "cache_read": 0.35,
            "input": 1.9,
            "output": 5.6
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1000000,
            "output": 131072
          },
          "modalities": {
            "input": [
              "text"
            ],
            "output": [
              "text"
            ]
          },
          "name": "GLM 5.3 [Cyber] [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "glm-5.3-default": {
          "attachment": false,
          "cost": {
            "cache_read": 0.036,
            "input": 0.15,
            "output": 0.45
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 128000
          },
          "modalities": {
            "input": [
              "text"
            ],
            "output": [
              "text"
            ]
          },
          "name": "GLM 5.3 Default",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "glm-5.3-flash-abliterated": {
          "attachment": false,
          "cost": {
            "cache_read": 0.2,
            "input": 0.45,
            "output": 0.95
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 32768
          },
          "modalities": {
            "input": [
              "text"
            ],
            "output": [
              "text"
            ]
          },
          "name": "GLM 5.3 Flash [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "gpt-6.1-sol": {
          "attachment": true,
          "cost": {
            "cache_read": 0.018,
            "input": 0.32,
            "output": 1.12
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 128000
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "GPT 6.1 Sol",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "kimi-k3.1-pro": {
          "attachment": false,
          "cost": {
            "cache_read": 0.04,
            "input": 0.35,
            "output": 1.45
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 128000
          },
          "modalities": {
            "input": [
              "text"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Kimi K3.1 Pro",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "kimi-k3.1-pro-abliterated": {
          "attachment": true,
          "cost": {
            "cache_read": 0.85,
            "input": 8.5,
            "output": 20.5
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 32768
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Kimi K3.1 Pro [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "zeus": {
          "attachment": true,
          "cost": {
            "cache_read": 0.032,
            "input": 0.85,
            "output": 2.5
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 32768
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Zeus [UNCENSORED]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        },
        "zeus-cheap": {
          "attachment": true,
          "cost": {
            "cache_read": 0.01,
            "input": 0.1,
            "output": 0.45
          },
          "interleaved": "reasoning_content",
          "limit": {
            "context": 1048576,
            "output": 128000
          },
          "modalities": {
            "input": [
              "text",
              "image"
            ],
            "output": [
              "text"
            ]
          },
          "name": "Zeus [CHEAP]",
          "reasoning": true,
          "temperature": true,
          "tool_call": true
        }
      },
      "name": "Small Paper",
      "npm": "@ai-sdk/openai-compatible",
      "options": {
        "apiKey": "{env:SMALLPAPER_API_KEY}",
        "baseURL": "https://api.smallpaper.io/v1"
      }
    }
  }
}
```

Set the key the config expects:

```sh
export SMALLPAPER_API_KEY=41920837556120947381      # your account number, no spaces
```

## What each field is doing

| field | why it matters |
|---|---|
| `npm` | `@ai-sdk/openai-compatible` is the right driver: we serve `/v1/chat/completions`, not `/v1/responses` |
| `apiKey` | `{env:SMALLPAPER_API_KEY}` keeps your account number out of the file |
| `tool_call` | true where the model calls tools; we translate them in both directions, so agent mode works |
| `reasoning` + `interleaved` | `"reasoning_content"` is the field our SSE actually emits, so opencode renders the thinking trace instead of discarding it |
| `temperature` | true where the model takes a sampling temperature; sampling parameters are forwarded as you send them |
| `attachment` | true where the model reads images, so opencode lets you attach them |
| `modalities` | image in only where the model reads images, text out; without it opencode 2 offers image input to every model |
| `cost` | **our** rates, not the upstream vendor's — so opencode's spend display matches what you are actually charged |
| `limit` | real context and output ceilings, so opencode truncates before we have to |

## Notes

- **Fund the account first.** A new account is empty, so opencode gets an
  `insufficient_balance` 402 until you top up. See [Quickstart](https://smallpaper.io/docs/quickstart.md).
- **Your system prompt is honoured.** opencode's agent definition reaches the model unchanged
  and stays first. See [API](https://smallpaper.io/docs/api.md).
- **No proof-of-work on the API.** The chat requires one per message; `/v1` never does.
- If a model shows up but every call fails, check `/api/me` for your balance first.
