Docs

Pi

Pi — Earendil's terminal coding agent, not the Inflection chatbot and not the coin — takes a custom provider as one block in models.json: a baseUrl, an api, a key and the model ids you want. Four fields and it is talking to Claude and GPT through one key.

A custom provider block in ~/.pi/agent/models.json — baseUrl, api and the model ids — puts Earendil's Pi terminal coding agent on Claude, GPT and Gemini through one key.

~/.pi/agent/models.json
{
  "providers": {
    "kunavo": {
      "baseUrl": "https://api.kunavo.com/v1",
      "api": "openai-completions",
      "apiKey": "$KUNAVO_API_KEY",
      "models": [
        {
          "id": "claude-sonnet-5",
          "name": "Claude Sonnet 5",
          "reasoning": true,
          "input": ["text", "image"],
          "contextWindow": 1000000,
          "maxTokens": 128000
        },
        {
          "id": "claude-haiku-4-5",
          "name": "Claude Haiku 4.5",
          "input": ["text", "image"],
          "contextWindow": 200000,
          "maxTokens": 64000
        }
      ]
    }
  }
}
Keep the /v1 on an openai-completions provider. Pi's Supported APIs table calls that value “OpenAI Chat Completions (most compatible)”, and every base URL its own page shows for it carries a version path — https://openrouter.ai/api/v1, https://ai-gateway.vercel.sh/v1, http://localhost:11434/v1. No sentence on the page states the rule outright, so the examples are what settles it, and the only place Pi documents adding a path for you is Azure-specific: “root endpoints are auto-normalized to /openai/v1”. A base URL without the suffix shows up as a 404, not an authentication error.
A custom model's cost defaults to all zeros, which Pi documents plainly — so the provider you just added reports $0 in the footer and in /session until you type the rates in yourself. The two silent defaults below it bite harder: contextWindow falls back to 128000 and maxTokens to 16384, so a model you left blank gets compacted and truncated far short of what it can actually take. The block above sets both from the catalog; do the same for any id you add from the table below.
Where Pi looks for the key, in its own order. The providers page lists credential resolution as the CLI --api-key flag, then an auth.json entry (API key or OAuth token), then an environment variable, then custom provider keys from models.json. So a stale key saved by /login wins over the one in your file, which is the usual reason a freshly-edited block still authenticates as something else. The same page adds that without any auth configured the models load but “stay unavailable in /model and --list-models” — a model that will not appear in the picker is a credential fault, not a syntax one.
This block was read off Pi's own documentation on the date below. Kunavo has not run Pi against its endpoint — not a session, not a streamed turn, not a tool round-trip, not a check of which model a request ended up on. A published setup page is a configuration reference, not a compatibility test, and nothing here should be read as one. The curl below is the part you can settle in ten seconds; the client's behaviour is between you and Pi.

Step by step

  1. Create a key at /app/keys and copy it — it is shown once.
  2. Put it in the environment as KUNAVO_API_KEY. Pi resolves "$ENV_VAR" or "${ENV_VAR}" in the apiKey field, and its own warning is worth heeding: $FOO_BAR is the variable FOO_BAR, so use the braced form when literal text follows the name. A missing variable leaves the value unresolved rather than erroring loudly.
  3. Create or edit ~/.pi/agent/models.json and paste the block above. A non-built-in provider needs baseUrl and an api value at either provider or model level; everything else in Pi's table is optional.
  4. Start pi, run /model and pick one of the ids you declared. If they are not listed, check the key before the JSON — see the resolution-order note above.
  5. Give it a task that reads and edits a real file. Pi leans on tool calling for nearly everything it does, so a first run that touches the filesystem tells you much more than a greeting does — and it is the run that would surface a streaming or tool-schema mismatch, which is exactly the class of thing Kunavo has not tested for you.

Checked against Pi's Custom Models documentation on September 21, 2026. Third-party settings move; if a field name here no longer matches what you see, that page is the authority, not this one.

This is the short version. The full walkthrough — model choice, what a real session costs, and the failure modes — is in what running Pi actually costs, route by route.

Verify before you debug the client

One request settles whether a failure is the endpoint, the key, or the configuration file. If this returns JSON, the same base URL and key work in Pi.

# Settles whether a failure is the endpoint, the key, or the client.
curl -sS https://api.kunavo.com/v1/models \
  -H "Authorization: Bearer sk-kn-..."

Which model id to put in the field

Every text model is reachable as a model id — the live list is GET /v1/models, and the catalog with prices is on the models page. Rates are USD per 1M tokens, input / output.

Model idKunavo in / outWhere it fits in Pi
claude-sonnet-5$2.00 / $10.00the default working model for sessions that edit files
claude-opus-5$2.00 / $10.00planning a change that would be expensive to get wrong
claude-haiku-4-5$0.40 / $2.00cheap turns — triage, summaries, the loop that runs all day
gpt-5-6-sol$2.00 / $12.00a second opinion from another family, same key and same baseUrl
Billing is per token from a prepaid balance with no monthly fee — see billing. On repeated context — which is most of what an editor or a chat client sends — prompt caching moves the bill more than the model choice does.

The other route: anthropic-messages

Pi's custom-provider schema accepts four values for apiopenai-completions, openai-responses, anthropic-messages and google-generative-ai — and Kunavo answers the Anthropic Messages surface as well as the OpenAI-compatible one. So the route is expressible. What this page will not do is write a base URL for it into a block you would paste, because Pi's own page shows that field two different ways and settles neither: "baseUrl": "https://proxy.example.com/v1" in its Custom Headers example, and a bare "baseUrl": "https://proxy.example.com" in its Anthropic Messages Compatibility example. Two readings, no sentence deciding between them. If you take this route, try one, and if the first call comes back 404 rather than 401, that line is the one to change.

Three more fields from the same section are worth knowing before you get there, because they exist precisely for endpoints that are not Anthropic itself:

  1. compat.supportsEagerToolInputStreaming — Pi sends per-tool eager_input_streaming by default and documents setting this to false when a backend rejects the field, which makes it fall back to the legacy fine-grained tool-streaming beta header.
  2. compat.supportsStrictTools — off by default for custom models; Pi says built-in Anthropic models enable it in their own metadata, so a custom Anthropic-compatible model has to set it explicitly if the endpoint accepts strict JSON-schema tool definitions.
  3. compat.supportsMidConvoEffort — Pi's own guidance is narrow and worth quoting: set it “only for the exact supported Claude model on a faithful Anthropic Messages transport”. Whether this endpoint qualifies is a runtime question, and Kunavo has not answered it by running anything.

The openai-completions block at the top of this page avoids all three, which is the honest reason to start there rather than a claim that it performs better.

FAQ

How do I point the Pi coding agent at a custom API provider?

Add a provider block to ~/.pi/agent/models.json. Pi's Custom Models page documents the fields in this order: baseUrl, api, apiKey, oauth, headers, authHeader, models, modelOverrides. A non-built-in provider needs baseUrl and an api value at either provider or model level; api is one of openai-completions, openai-responses, anthropic-messages or google-generative-ai. Each entry in models needs at least an id, which is passed straight to the endpoint. Pi's providers page describes this as adding "any provider that speaks a supported API", so the same shape covers a gateway, a local Ollama or vLLM server, and anything else on that list.

Where does the Pi coding agent get its API key from?

From one of four places, and Pi's providers page publishes the order it tries them: the CLI --api-key flag, then an auth.json entry (API key or OAuth token), then an environment variable, then custom provider keys from models.json. The apiKey field in models.json therefore has the lowest priority, which is why a key saved earlier with /login can quietly win over the one you just edited in. The field itself supports environment interpolation — "$ENV_VAR" or "${ENV_VAR}" — as well as shell command output with a leading "!", so the secret need not sit in the file. With no auth configured at all, Pi's documentation says the models load but stay unavailable in /model and --list-models.

Does Pi's baseUrl need /v1 at the end?

For an openai-completions provider, yes. Pi's Custom Models page never states the rule in a sentence, but every base URL it shows for that api carries a version path — http://localhost:11434/v1 for Ollama, https://openrouter.ai/api/v1, https://ai-gateway.vercel.sh/v1, http://localhost:8080/v1 for llama.cpp — and the only path Pi documents adding on your behalf is the Azure-specific normalization to /openai/v1. So the value for an OpenAI-compatible endpoint is the /v1 root, for example https://api.kunavo.com/v1. The anthropic-messages case is genuinely unsettled: the same page shows https://proxy.example.com/v1 in one example and a bare https://proxy.example.com in another, with no sentence choosing between them.

Why does my custom Pi provider show $0 in the footer?

Because Pi defaults a custom model's cost object to all zeros, and the footer reports what the catalog says rather than what the endpoint charges. Nothing is free; the number simply has no source until you fill in the per-million input, output, cacheRead and cacheWrite rates, plus any tiers, from your provider's own price list. Two neighbouring defaults deserve the same treatment while you are in the file: contextWindow falls back to 128000 and maxTokens to 16384, so a model with a larger window gets compacted early and its replies cut short unless both are set explicitly.

Has Kunavo tested the Pi coding agent?

No. The configuration on this page was read from Pi's own documentation on the date shown and re-checked against the live page, but no session, streamed turn, tool round-trip or model-routing check has been run against this endpoint with this client — and the same is true of every client in this section. Treat the setup block as a reference for what Pi's schema accepts, verify the endpoint and key with the curl above, and keep a working route available while you try it.