Skip to content
Numes

Integrations

Coding agents

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

On this page

Coding agents that support custom OpenAI-compatible providers run on Numes without code changes. numes run configures and starts OpenCode, Pi or OMP for you; the rest of this page shows the same configuration by hand.

AgentTested versionProvider type
OpenCode1.18.22 and 2.1.4@openrouter/ai-sdk-provider (OpenCode 2: @opencode/ai/providers/openrouter)
Pi1.4.0openai-completions
OMP18.0.11openai-completions

Other agents that speak Chat Completions work the same way; see Integrations.

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.

  1. Install the command

    Install numes once, as described in Install and update. It's the same command providers use, but you don't need to set up a provider to use numes run.

    Shell
    curl -fsSL https://numes.ai/install | sh
  2. Install an agent

    numes run starts agents you already have; it doesn't install them. Install OpenCode, 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.

  3. Start it

    Shell
    numes run opencode

    The first launch asks how to connect:

    • Paste an existing inference key (the default). Create one in 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:

Shell
numes run opencode --continuenumes 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.

AgentCommandHow it's configured
OpenCodenumes run opencodeThe 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
Pinumes run piRuns 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
OMPnumes run ompLike 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 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

Shell
numes auth key status   # the saved key's name, currency, limit and statenumes auth key set      # paste or create a different keynumes auth key remove   # forget the saved key on this machinenumes 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. 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:

Shell
NUMES_API_KEY="sk-..." numes run pi

What is stored where

PathContents
~/.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

  1. Create an inference key

    Create a key in Keys with a spending limit, and export it:

    Shell
    export NUMES_API_KEY="sk-..."
  2. 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:

    curl "https://api.numes.ai/api/v1/models?supported_parameters=tools" \  -H "Authorization: Bearer $NUMES_API_KEY"
  3. 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:

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 }        }      }    }  }}
FieldSet it from GET /models
Model keyThe model's id
tool_calltrue when supported_parameters includes tools
reasoningtrue when supported_parameters includes reasoning
temperaturetrue when supported_parameters includes temperature
attachmenttrue when architecture.input_modalities includes image or file
limit.contextcontext_length
limit.outputtop_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:

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:

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:

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:

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 }        }      }    }  }}
FieldSet it from GET /models
capabilities.toolstrue when supported_parameters includes tools
capabilities.inputarchitecture.input_modalities, with file written as pdf
capabilities.temperaturetrue 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
costinput, 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.

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

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.

Shell
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 $:

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        }      ]    }  }}
Shell
PI_CODING_AGENT_DIR="$HOME/.config/pi-marketplace" \  pi --provider marketplace --model openai/gpt-6-luna
SettingWhy
api: "openai-completions"Use Chat Completions
authHeader: trueSend the key as a bearer token
supportsStore: falseDon't send store, which the API rejects
supportsDeveloperRole: falseSend instructions as system messages
supportsReasoningEffort: falseDon'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:

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
Shell
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.

  • Cost. Every response reports usage.cost in the key's currency. Agent-side cost displays are estimates; Activity and Logs show what you were charged.

Troubleshooting

SymptomLikely causeFix
400 invalid_request on every request to some modelsOpenCode sent reasoningEffortInstall the reasoning-effort plugin
400 invalid_request on all requestsAn unsupported field such as store, or an output limit above 131,072Check the compat settings; lower limit.output or maxTokens
401 invalid_credentialsA key from another service, a management key, or a disabled or expired keyUse an active inference key from Keys
Pi returns 401 invalid_credentials with an active inference keyapiKey is a bare variable namePrefix the name with $, as in the Pi configuration; for numes run pi, update numes and launch again to regenerate the file
402 insufficient_creditsYour balance or spending limit doesn't cover the amount held for the requestAdd credits, raise the spending limit, or wait for running requests to finish
503 provider_unavailable right awayAn unknown model, one not priced in the key's currency, or one without tool supportCopy the ID from GET /models?supported_parameters=tools; use a text-only agent for models without tools
429 rate_limited during long sessionsA key or account rate limitWait for Retry-After
413 payload_too_largeA conversation over the 10 MiB request limitCompact it or start a new session
Costs look wrongA BTC key reports costs in satoshisRemove 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.

Next steps