# Coding agents

> Start OpenCode, Pi and OMP on Numes with one command, or configure them by hand as an OpenAI-compatible provider.

Source: https://0.0.0.0:3000/docs/integrations/coding-agents
Documentation index: https://0.0.0.0:3000/docs/llms.txt

Coding agents that support custom OpenAI-compatible providers run on Numes without code changes. [`numes run`](#quick-start-with-numes-run) configures and starts **OpenCode**, **Pi** or **OMP** for you; the rest of this page shows the same configuration by hand.

| Agent | Tested version | Provider type |
| --- | --- | --- |
| OpenCode | 1.18.22 and 2.1.4 | `@openrouter/ai-sdk-provider` (OpenCode 2: `@opencode/ai/providers/openrouter`) |
| Pi | 1.4.0 | `openai-completions` |
| OMP | 18.0.11 | `openai-completions` |

Other agents that speak Chat Completions work the same way; see [Integrations](/docs/integrations/overview).

## Quick start with numes run

On every launch, `numes run` reads the models your key can use, writes the agent's configuration from that list, and starts the agent with your key.

:::steps
### Install the command

Install `numes` once, as described in [Install and update](/docs/providers/install-and-update). It's the same command providers use, but you don't need to set up a provider to use `numes run`.

```bash
curl -fsSL https://numes.ai/install | sh
```

### Install an agent

`numes run` starts agents you already have; it doesn't install them. Install [OpenCode](https://opencode.ai), Pi (`npm install -g @earendil-works/pi-coding-agent`) or OMP (`npm install -g @oh-my-pi/pi-coding-agent`). Run `numes run` on its own to see which agents are installed and the versions Numes tests with. Other versions start with a warning.

### Start it

```bash
numes run opencode
```

The first launch asks how to connect:

- **Paste an existing inference key** (the default). Create one in [Keys](/dashboard/keys) with a spending limit. The key is checked before it's saved.
- **Sign in and create a new key.** Signs you in through your browser, then creates a key in the account and currency you choose, with an optional spending limit. You're warned when that currency has no balance.

Later launches reuse the saved key.
:::

Everything after the agent's name goes to the agent unchanged:

```bash
numes run opencode --continue
numes run pi --model openai/gpt-6-luna
```

OpenCode 2 also accepts global options before a subcommand, for example
`numes run opencode --print-logs models`. The launcher starts a private server
for supported commands, including `reload`; `pair` keeps OpenCode's own server
behavior.

| Agent | Command | How it's configured |
| --- | --- | --- |
| OpenCode | `numes run opencode` | The generated configuration is merged over your own OpenCode settings, which stay in effect. OpenCode 2 runs on a private server for that launch, so its shared background server and your other sessions are unaffected |
| Pi | `numes run pi` | Runs with its own Pi home, so its sessions and settings are separate from a Pi you use elsewhere. Sessions persist between runs with regular models. The development encrypted mock uses an isolated home for each launch |
| OMP | `numes run omp` | Like Pi, with its own OMP home |

Each agent starts on a tool-capable model unless you choose a model, provider or session yourself.

Models shared with one of your organizations appear under a separate provider named after the organization. OpenCode also lists [encrypted models](/docs/inference/encrypted) under their own provider; Pi and OMP support the development Synthetic E2EE Mock through the local bridge; attested encrypted models remain unavailable in those clients.

### Manage the key

```bash
numes auth key status   # the saved key's name, currency, limit and state
numes auth key set      # paste or create a different key
numes auth key remove   # forget the saved key on this machine
numes run --replace-key opencode
```

When you replace or remove a key that `numes` created, it offers to revoke it; a pasted key keeps working until you revoke it in [Keys](/dashboard/keys). If a launch finds the saved key rejected, it asks for a new one. For scripts, pass the answers as options, for example `numes auth key set --create --account ACCOUNT_ID --currency USD --limit 20`; `numes account list` shows your account IDs.

To use a key without saving it, set it in the environment, where it takes precedence over the saved key:

```bash
NUMES_API_KEY="sk-..." numes run pi
```

### What is stored where

| Path | Contents |
| --- | --- |
| `~/.config/numes/apps/` | The saved key, readable only by you, and the generated agent configuration. Follows `XDG_CONFIG_HOME` |
| `~/.local/share/numes/apps/` | The Pi and OMP homes, including their sessions, and OpenCode's encrypted-session markers. Follows `XDG_DATA_HOME` |

Keys are saved per Numes deployment, so a key is only sent to the deployment that issued it. Generated files never contain the key: agents read it from `NUMES_API_KEY`, which `numes run` sets for them. `numes uninstall` keeps these files; `numes uninstall --purge --confirm` deletes them without revoking any key.

## Configure an agent by hand

:::steps
### Create an inference key

Create a key in [Keys](/dashboard/keys) with a spending limit, and export it:

```bash
export NUMES_API_KEY="sk-..."
```

### Choose tool-capable models

Agents send tool declarations with every request, and a model without tool support fails with `503 provider_unavailable`. List the models your key can use that support tools:

```request
{
  "method": "GET",
  "path": "/models",
  "query": { "supported_parameters": "tools" }
}
```

### Note each model's limits

Record each model's `context_length` and `top_provider.max_completion_tokens`. The agent's output limit must not exceed 131,072, or requests fail with `400`. Use `top_provider.max_completion_tokens` when it's set, otherwise a conservative value such as 8,192.
:::

Agents prefix model IDs with the provider name you choose, such as `marketplace/openai/gpt-6-luna` in OpenCode. The prefix only selects the provider inside the agent; the API receives the bare ID. Model lists change as providers come and go, so refresh your configuration from `GET /models` from time to time.

## OpenCode

OpenCode uses the OpenRouter provider package pointed at Numes. Add a provider to `opencode.json` in your project, or to the file named by `OPENCODE_CONFIG`:

```json title="opencode.json"
{
  "$schema": "https://opencode.ai/config.json",
  "model": "marketplace/openai/gpt-6-luna",
  "provider": {
    "marketplace": {
      "npm": "@openrouter/ai-sdk-provider",
      "name": "Numes",
      "options": {
        "baseURL": "https://api.numes.ai/api/v1",
        "apiKey": "{env:NUMES_API_KEY}"
      },
      "models": {
        "openai/gpt-6-luna": {
          "name": "Example model",
          "tool_call": true,
          "reasoning": false,
          "temperature": true,
          "attachment": false,
          "modalities": { "input": ["text"], "output": ["text"] },
          "limit": { "context": 131072, "output": 8192 }
        }
      }
    }
  }
}
```

| Field | Set it from `GET /models` |
| --- | --- |
| Model key | The model's `id` |
| `tool_call` | `true` when `supported_parameters` includes `tools` |
| `reasoning` | `true` when `supported_parameters` includes `reasoning` |
| `temperature` | `true` when `supported_parameters` includes `temperature` |
| `attachment` | `true` when `architecture.input_modalities` includes `image` or `file` |
| `limit.context` | `context_length` |
| `limit.output` | `top_provider.max_completion_tokens`, or a conservative value when it's `null` |

`{env:NUMES_API_KEY}` makes OpenCode read the key from the environment, so it never sits in the file. Start OpenCode and pick the model, or pass `--model marketplace/openai/gpt-6-luna`.

For cost estimates in OpenCode, add a `cost` object (`input`, `output`, `cache_read`, `cache_write`) in dollars per million tokens: multiply the per-token catalogue prices by one million. The actual charge is `usage.cost`. Omit `cost` for BTC keys, whose prices are in satoshis.

### Reasoning models

For a model whose `supported_parameters` include `reasoning`, set `"reasoning": true` and declare a variant for each effort in its `reasoning.supported_efforts`:

```json
"variants": {
  "low": { "reasoning": { "effort": "low" } },
  "medium": { "reasoning": { "effort": "medium" } },
  "high": { "reasoning": { "effort": "high" } }
}
```

### Reasoning effort in OpenCode

OpenCode applies built-in defaults for some model names that set the option `reasoningEffort`. The OpenRouter package sends that option as an unknown field, so every request to those models fails with `400 invalid_request` while other models work.

This OpenCode plugin removes `reasoningEffort` and, where the model declares a matching variant, sends it as `reasoning.effort` instead. Save it next to your configuration:

```javascript title="reasoning-effort.mjs"
// Rewrites OpenCode's reasoningEffort option into the API's reasoning object.
export default async function reasoningEffort(_context, { providerId }) {
  return {
    "chat.params": async ({ model }, { options }) => {
      if (
        model.providerID !== providerId ||
        model.api.npm !== "@openrouter/ai-sdk-provider"
      )
        return;
      const effort = options.reasoningEffort;
      delete options.reasoningEffort;
      if (
        model.capabilities.reasoning &&
        effort !== undefined &&
        model.variants?.[effort] !== undefined &&
        options.reasoning === undefined
      ) {
        options.reasoning = { effort };
      }
    },
  };
}
```

Register it with your provider ID:

```json title="opencode.json"
{
  "plugin": [["/path/to/reasoning-effort.mjs", { "providerId": "marketplace" }]]
}
```

### Models without tool calling

OpenCode's normal agents declare tools on every request. To use a model without tool support, define an agent that denies every permission, so no tools are declared:

```json title="opencode.json"
{
  "agent": {
    "text-only": {
      "description": "Text-only mode for models without tool calling",
      "mode": "primary",
      "permission": { "*": "deny" }
    }
  }
}
```

### OpenCode 2

OpenCode 2 still reads the configuration above, but it ignores `reasoning`, `temperature` and `attachment`, and it can't load the reasoning-effort plugin. It doesn't need the plugin: its own OpenRouter driver sends the selected effort as `reasoning.effort`. Its native format declares the same facts as capabilities:

```json title="opencode.json"
{
  "$schema": "https://opencode.ai/config.json",
  "model": "marketplace/openai/gpt-6-luna",
  "providers": {
    "marketplace": {
      "name": "Numes",
      "package": "@opencode/ai/providers/openrouter",
      "settings": {
        "baseURL": "https://api.numes.ai/api/v1",
        "apiKey": "{env:NUMES_API_KEY}"
      },
      "models": {
        "openai/gpt-6-luna": {
          "name": "Example model",
          "capabilities": {
            "tools": true,
            "input": ["text"],
            "output": ["text"],
            "temperature": true,
            "reasoning": { "supported": true, "effort": ["low", "medium", "high"] }
          },
          "limit": { "context": 131072, "output": 8192 }
        }
      }
    }
  }
}
```

| Field | Set it from `GET /models` |
| --- | --- |
| `capabilities.tools` | `true` when `supported_parameters` includes `tools` |
| `capabilities.input` | `architecture.input_modalities`, with `file` written as `pdf` |
| `capabilities.temperature` | `true` when `supported_parameters` includes `temperature` |
| `capabilities.reasoning` | `{ "supported": true, "effort": [...] }` from `reasoning.supported_efforts` when `supported_parameters` includes `reasoning`, otherwise `{ "supported": false }`. OpenCode builds one variant per effort |
| `cost` | `input`, `output` and `cache` (`read`, `write`) in dollars per million tokens, as above |

Leave `compatibility.supportsPromptCacheKey` unset on these models. OpenCode 2 then sends its session ID as `prompt_cache_key`, which gives the conversation session affinity on every model, whether or not the model lists `prompt_cache_key` in `supported_parameters`. The key never narrows routing, and stickiness follows the usual rules, for example it is off when you set `provider.order` or `provider.sort`. Only endpoints that accept a cache key receive a hint from it. See [Sessions](/docs/features/sessions).

For models without tool calling, the agent's permissions are a list of rules:

```json title="opencode.json"
{
  "agents": {
    "text-only": {
      "description": "Text-only mode for models without tool calling",
      "mode": "primary",
      "permissions": [{ "action": "*", "resource": "*", "effect": "deny" }]
    }
  }
}
```

OpenCode 2 connects to a shared background server, which keeps the environment it started with. `OPENCODE_CONFIG` and `NUMES_API_KEY` set in your shell reach it only if they were set when it started. To use your current environment, start OpenCode with a private server: `opencode --standalone`, or `opencode run --standalone` for one message.

```bash
opencode --agent text-only --model marketplace/author/text-model
```

## Pi

Pi reads custom providers from `models.json` in its agent directory, which `PI_CODING_AGENT_DIR` selects. Prefix the environment variable name with `$` in `apiKey`, so Pi reads the key from the environment. A bare variable name is sent as a literal key and causes `401 invalid_credentials`. Environment references in `headers` also require `$`:

```json title="models.json"
{
  "providers": {
    "marketplace": {
      "baseUrl": "https://api.numes.ai/api/v1",
      "api": "openai-completions",
      "apiKey": "$NUMES_API_KEY",
      "authHeader": true,
      "compat": {
        "supportsStore": false,
        "supportsDeveloperRole": false,
        "supportsReasoningEffort": false,
        "maxTokensField": "max_tokens"
      },
      "models": [
        {
          "id": "openai/gpt-6-luna",
          "name": "Example model",
          "reasoning": false,
          "input": ["text"],
          "contextWindow": 131072,
          "maxTokens": 8192
        }
      ]
    }
  }
}
```

```bash
PI_CODING_AGENT_DIR="$HOME/.config/pi-marketplace" \
  pi --provider marketplace --model openai/gpt-6-luna
```

| Setting | Why |
| --- | --- |
| `api: "openai-completions"` | Use Chat Completions |
| `authHeader: true` | Send the key as a bearer token |
| `supportsStore: false` | Don't send `store`, which the API rejects |
| `supportsDeveloperRole: false` | Send instructions as `system` messages |
| `supportsReasoningEffort: false` | Don't send reasoning controls unless a model opts in |
| `maxTokensField: "max_tokens"` | Name of the output-limit field |

Set `input` to `["text", "image"]` for models whose `input_modalities` include `image`, and keep `maxTokens` within `top_provider.max_completion_tokens`.

For a reasoning model, opt in per model and map Pi's thinking levels to the efforts the model supports, with `null` for levels it doesn't offer:

```json
{
  "id": "author/reasoning-model",
  "name": "Reasoning model",
  "reasoning": true,
  "input": ["text"],
  "contextWindow": 131072,
  "maxTokens": 16384,
  "compat": { "supportsReasoningEffort": true, "thinkingFormat": "openrouter" },
  "thinkingLevelMap": {
    "off": "none",
    "minimal": null,
    "low": "low",
    "medium": "medium",
    "high": "high",
    "xhigh": null,
    "max": null
  }
}
```

Map `"off"` to `"none"` only when the model's `reasoning.mandatory` is `false` or its `supported_efforts` include `none`; otherwise set it to `null`, because reasoning can't be turned off.

## OMP

OMP takes the same provider shape in YAML, but its `apiKey` uses the bare environment variable name. Save it as `models.yml` and pass it with `--config`:

```yaml title="models.yml"
providers:
  marketplace:
    baseUrl: "https://api.numes.ai/api/v1"
    api: openai-completions
    apiKey: "NUMES_API_KEY"
    authHeader: true
    compat:
      supportsStore: false
      supportsDeveloperRole: false
      supportsReasoningEffort: false
      maxTokensField: max_tokens
    models:
      - id: "openai/gpt-6-luna"
        name: Example model
        reasoning: false
        input: [text]
        contextWindow: 131072
        maxTokens: 8192
        supportsTools: true
```

```bash
omp --config models.yml --provider marketplace --model openai/gpt-6-luna
```

Set `supportsTools` from whether the model's `supported_parameters` include `tools`. For a reasoning model, add `compat: { supportsReasoningEffort: true, thinkingFormat: openrouter }` and a `thinking` block listing the supported efforts other than `none`:

```yaml
      - id: "author/reasoning-model"
        name: Reasoning model
        reasoning: true
        input: [text]
        contextWindow: 131072
        maxTokens: 16384
        supportsTools: true
        compat:
          supportsReasoningEffort: true
          thinkingFormat: openrouter
        thinking:
          mode: effort
          efforts: [low, medium, high]
          defaultLevel: medium
```

## What agents need from the API

- **Streaming.** Each stream ends with a usage-only chunk whose `choices` array is empty, then `[DONE]`. The tested versions handle it.
- **Reasoning continuation.** Agents that send the assistant message back unchanged, including `reasoning_details`, keep the reasoning chain intact across tool calls.
- **Session affinity.** Agents that send `session_id`, `prompt_cache_key` or `X-Session-Id` stay on the same endpoint while the conversation is active, which improves prompt-cache hits. See [Sessions](/docs/features/sessions).
- **Cost.** Every response reports `usage.cost` in the key's currency. Agent-side cost displays are estimates; [Activity](/dashboard/activity) and [Logs](/dashboard/logs) show what you were charged.

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| `400 invalid_request` on every request to some models | OpenCode sent `reasoningEffort` | Install the [reasoning-effort plugin](#reasoning-effort-in-opencode) |
| `400 invalid_request` on all requests | An unsupported field such as `store`, or an output limit above 131,072 | Check the `compat` settings; lower `limit.output` or `maxTokens` |
| `401 invalid_credentials` | A key from another service, a management key, or a disabled or expired key | Use an active inference key from [Keys](/dashboard/keys) |
| Pi returns `401 invalid_credentials` with an active inference key | `apiKey` is a bare variable name | Prefix the name with `$`, as in the [Pi configuration](#pi); for `numes run pi`, update `numes` and launch again to regenerate the file |
| `402 insufficient_credits` | Your balance or spending limit doesn't cover the amount held for the request | Add credits, raise the spending limit, or wait for running requests to finish |
| `503 provider_unavailable` right away | An unknown model, one not priced in the key's currency, or one without tool support | Copy the ID from `GET /models?supported_parameters=tools`; use a text-only agent for models without tools |
| `429 rate_limited` during long sessions | A key or account rate limit | Wait for `Retry-After` |
| `413 payload_too_large` | A conversation over the 10 MiB request limit | Compact it or start a new session |
| Costs look wrong | A BTC key reports costs in satoshis | Remove `cost` from the agent configuration, or read costs as satoshis |

For any other error, note the `X-Request-Id` if the agent shows it, and find the request in [Logs](/dashboard/logs).

## Next steps

:::cards
- {plug} [Integrations](/docs/integrations/overview) Settings for any OpenAI-compatible client.
- {wrench} [Tool calling](/docs/features/tool-calling) How tools and tool choice work.
- {brain} [Reasoning](/docs/features/reasoning) Effort levels and reasoning continuation.
- {database} [Prompt caching](/docs/features/prompt-caching) Lower the cost of long agent sessions.
- {key} [API keys](/docs/account/api-keys) Give each agent its own key and limit.
:::
