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
- Quick start with numes run
- Install the command
- Install an agent
- Start it
- Manage the key
- What is stored where
- Configure an agent by hand
- Create an inference key
- Choose tool-capable models
- Note each model's limits
- OpenCode
- Reasoning models
- Reasoning effort in OpenCode
- Models without tool calling
- OpenCode 2
- Pi
- OMP
- What agents need from the API
- Troubleshooting
- Next steps
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.
| 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.
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.
Install the command
Install
numesonce, as described in Install and update. It's the same command providers use, but you don't need to set up a provider to usenumes run.Shellcurl -fsSL https://numes.ai/install | shInstall an agent
numes runstarts 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). Runnumes runon its own to see which agents are installed and the versions Numes tests with. Other versions start with a warning.Start it
Shellnumes run opencodeThe 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:
numes run opencode --continuenumes run pi --model openai/gpt-6-lunaOpenCode 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 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
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 opencodeWhen 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:
NUMES_API_KEY="sk-..." numes run piWhat 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
Create an inference key
Create a key in Keys with a spending limit, and export it:
Shellexport 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:curl "https://api.numes.ai/api/v1/models?supported_parameters=tools" \ -H "Authorization: Bearer $NUMES_API_KEY"
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:
{ "$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:
"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:
// 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:
{ "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:
{ "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:
{ "$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.
For models without tool calling, the agent's permissions are a list of rules:
{ "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.
opencode --agent text-only --model marketplace/author/text-modelPi
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 $:
{ "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 } ] } }}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:
{ "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:
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: trueomp --config models.yml --provider marketplace --model openai/gpt-6-lunaSet 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:
- 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: mediumWhat agents need from the API
Streaming. Each stream ends with a usage-only chunk whose
choicesarray 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_keyorX-Session-Idstay on the same endpoint while the conversation is active, which improves prompt-cache hits. See Sessions.Cost. Every response reports
usage.costin the key's currency. Agent-side cost displays are estimates; Activity and 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 |
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 |
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; 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.