> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bettertoken.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Pi Agent third-party API setup: Base URL, API key, and models

> Connect Pi Agent to a third-party API with models.json, a custom Base URL, API key, and Model ID. Configure protocols, verify access, and fix common errors.

export const PiModelPicker = ({locale = "en", plazaUrl = "https://bettertoken.ai/pricing"}) => {
  const translations = {
    en: {
      get: "Get model list",
      refresh: "Refresh model list",
      loading: "Loading models...",
      api: "API",
      search: "Search models",
      models: "Models",
      selected: "Selected",
      select: "Select",
      copyId: "Copy Model ID",
      copyConfig: "Copy configuration",
      download: "Download models.json",
      copied: "Copied",
      copyError: "Copy failed. Select and copy the text below.",
      error: "Could not load models. Try again or open the model plaza.",
      empty: "No matching models.",
      plaza: "Model plaza",
      config: "Pi configuration",
      selectAll: "Select all visible models"
    },
    zh: {
      get: "获取模型列表",
      refresh: "刷新模型列表",
      loading: "正在获取模型...",
      api: "接口",
      search: "搜索模型",
      models: "模型",
      selected: "已选择",
      select: "选择",
      copyId: "复制 Model ID",
      copyConfig: "复制配置",
      download: "下载 models.json",
      copied: "已复制",
      copyError: "复制失败，请选中下方文本复制。",
      error: "暂时无法获取模型，请重试或前往模型广场。",
      empty: "没有匹配的模型。",
      plaza: "模型广场",
      config: "Pi 配置",
      selectAll: "选择当前显示的全部模型"
    },
    ru: {
      get: "Получить список моделей",
      refresh: "Обновить список моделей",
      loading: "Загрузка моделей...",
      api: "API",
      search: "Поиск моделей",
      models: "Модели",
      selected: "Выбрано",
      select: "Выбрать",
      copyId: "Скопировать Model ID",
      copyConfig: "Скопировать настройки",
      download: "Скачать models.json",
      copied: "Скопировано",
      copyError: "Не удалось скопировать. Выделите и скопируйте текст ниже.",
      error: "Не удалось загрузить модели. Повторите попытку или откройте каталог моделей.",
      empty: "Подходящих моделей нет.",
      plaza: "Каталог моделей",
      config: "Настройки Pi",
      selectAll: "Выбрать все показанные модели"
    },
    hi: {
      get: "मॉडल सूची प्राप्त करें",
      refresh: "मॉडल सूची रीफ़्रेश करें",
      loading: "मॉडल लोड हो रहे हैं...",
      api: "API",
      search: "मॉडल खोजें",
      models: "मॉडल",
      selected: "चुने गए",
      select: "चुनें",
      copyId: "Model ID कॉपी करें",
      copyConfig: "कॉन्फ़िगरेशन कॉपी करें",
      download: "models.json डाउनलोड करें",
      copied: "कॉपी हो गया",
      copyError: "कॉपी नहीं हो सका। नीचे दिए गए टेक्स्ट को चुनकर कॉपी करें।",
      error: "मॉडल लोड नहीं हो सके। फिर से प्रयास करें या मॉडल कैटलॉग खोलें।",
      empty: "कोई मेल खाने वाला मॉडल नहीं मिला।",
      plaza: "मॉडल कैटलॉग",
      config: "Pi कॉन्फ़िगरेशन",
      selectAll: "दिखाए गए सभी मॉडल चुनें"
    },
    es: {
      get: "Obtener lista de modelos",
      refresh: "Actualizar lista de modelos",
      loading: "Cargando modelos...",
      api: "API",
      search: "Buscar modelos",
      models: "Modelos",
      selected: "Seleccionados",
      select: "Seleccionar",
      copyId: "Copiar Model ID",
      copyConfig: "Copiar configuración",
      download: "Descargar models.json",
      copied: "Copiado",
      copyError: "No se pudo copiar. Selecciona y copia el texto de abajo.",
      error: "No se pudieron cargar los modelos. Inténtalo de nuevo o abre el catálogo.",
      empty: "No hay modelos coincidentes.",
      plaza: "Catálogo de modelos",
      config: "Configuración de Pi",
      selectAll: "Seleccionar todos los modelos visibles"
    },
    "pt-br": {
      get: "Obter lista de modelos",
      refresh: "Atualizar lista de modelos",
      loading: "Carregando modelos...",
      api: "API",
      search: "Buscar modelos",
      models: "Modelos",
      selected: "Selecionados",
      select: "Selecionar",
      copyId: "Copiar Model ID",
      copyConfig: "Copiar configuração",
      download: "Baixar models.json",
      copied: "Copiado",
      copyError: "Não foi possível copiar. Selecione e copie o texto abaixo.",
      error: "Não foi possível carregar os modelos. Tente novamente ou abra o catálogo.",
      empty: "Nenhum modelo correspondente.",
      plaza: "Catálogo de modelos",
      config: "Configuração do Pi",
      selectAll: "Selecionar todos os modelos visíveis"
    },
    ja: {
      get: "モデル一覧を取得",
      refresh: "モデル一覧を更新",
      loading: "モデルを取得中...",
      api: "API",
      search: "モデルを検索",
      models: "モデル",
      selected: "選択済み",
      select: "選択",
      copyId: "Model ID をコピー",
      copyConfig: "設定をコピー",
      download: "models.json をダウンロード",
      copied: "コピーしました",
      copyError: "コピーできませんでした。下のテキストを選択してコピーしてください。",
      error: "モデルを取得できませんでした。再試行するか、モデルカタログを開いてください。",
      empty: "一致するモデルがありません。",
      plaza: "モデルカタログ",
      config: "Pi の設定",
      selectAll: "表示中のモデルをすべて選択"
    },
    fr: {
      get: "Obtenir la liste des modèles",
      refresh: "Actualiser les modèles",
      loading: "Chargement des modèles...",
      api: "API",
      search: "Rechercher des modèles",
      models: "Modèles",
      selected: "Sélectionnés",
      select: "Sélectionner",
      copyId: "Copier le Model ID",
      copyConfig: "Copier la configuration",
      download: "Télécharger models.json",
      copied: "Copié",
      copyError: "La copie a échoué. Sélectionnez et copiez le texte ci-dessous.",
      error: "Impossible de charger les modèles. Réessayez ou ouvrez le catalogue.",
      empty: "Aucun modèle correspondant.",
      plaza: "Catalogue de modèles",
      config: "Configuration de Pi",
      selectAll: "Sélectionner tous les modèles affichés"
    },
    de: {
      get: "Modellliste abrufen",
      refresh: "Modellliste aktualisieren",
      loading: "Modelle werden geladen...",
      api: "API",
      search: "Modelle suchen",
      models: "Modelle",
      selected: "Ausgewählt",
      select: "Auswählen",
      copyId: "Model ID kopieren",
      copyConfig: "Konfiguration kopieren",
      download: "models.json herunterladen",
      copied: "Kopiert",
      copyError: "Kopieren fehlgeschlagen. Markiere und kopiere den Text unten.",
      error: "Modelle konnten nicht geladen werden. Versuche es erneut oder öffne den Modellkatalog.",
      empty: "Keine passenden Modelle.",
      plaza: "Modellkatalog",
      config: "Pi-Konfiguration",
      selectAll: "Alle angezeigten Modelle auswählen"
    },
    ko: {
      get: "모델 목록 가져오기",
      refresh: "모델 목록 새로고침",
      loading: "모델을 가져오는 중...",
      api: "API",
      search: "모델 검색",
      models: "모델",
      selected: "선택됨",
      select: "선택",
      copyId: "Model ID 복사",
      copyConfig: "설정 복사",
      download: "models.json 다운로드",
      copied: "복사됨",
      copyError: "복사하지 못했습니다. 아래 텍스트를 선택하여 복사하세요.",
      error: "모델을 가져오지 못했습니다. 다시 시도하거나 모델 카탈로그를 여세요.",
      empty: "일치하는 모델이 없습니다.",
      plaza: "모델 카탈로그",
      config: "Pi 설정",
      selectAll: "표시된 모든 모델 선택"
    }
  };
  const t = translations[locale] || translations.en;
  const protocols = {
    "openai-responses": {
      label: "Responses",
      endpoint: "openai-response",
      baseUrl: "https://bettertoken.ai/v1"
    },
    "openai-completions": {
      label: "Chat Completions",
      endpoint: "openai",
      baseUrl: "https://bettertoken.ai/v1"
    },
    "anthropic-messages": {
      label: "Anthropic Messages",
      endpoint: "anthropic",
      baseUrl: "https://bettertoken.ai"
    }
  };
  const [models, setModels] = useState(null);
  const [api, setApi] = useState("openai-completions");
  const [search, setSearch] = useState("");
  const [selected, setSelected] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);
  const [feedback, setFeedback] = useState("");
  const request = useRef(null);
  const protocolGroup = useId();
  useEffect(() => () => {
    request.current?.abort();
    request.current = null;
  }, []);
  const eligible = (models || []).filter(model => model.endpoints.includes(protocols[api].endpoint));
  const visible = eligible.filter(model => model.id.toLowerCase().includes(search.trim().toLowerCase()));
  const chosen = eligible.filter(model => selected.includes(model.id));
  const configuration = JSON.stringify({
    providers: {
      bettertoken: {
        baseUrl: protocols[api].baseUrl,
        api,
        apiKey: "$BETTERTOKEN_API_KEY",
        models: chosen.map(model => ({
          id: model.id
        }))
      }
    }
  }, null, 2);
  const loadModels = async () => {
    if (request.current) return;
    const controller = new AbortController();
    request.current = controller;
    setLoading(true);
    setError(false);
    setFeedback("");
    const timer = setTimeout(() => controller.abort(), 15000);
    try {
      const response = await fetch("https://bettertoken.ai/api/pricing", {
        credentials: "omit",
        cache: "no-store",
        headers: {
          Accept: "application/json"
        },
        signal: controller.signal
      });
      if (!response.ok) throw new Error("Catalog request failed");
      const body = await response.json();
      if (body.success !== true || !Array.isArray(body.data)) throw new Error("Invalid catalog");
      const catalog = new Map();
      for (const entry of body.data) {
        if (typeof entry?.model_name !== "string" || !entry.model_name.trim() || !Array.isArray(entry.supported_endpoint_types)) continue;
        if ((/^(?:nano-banana-|gpt-image-)/i).test(entry.model_name)) continue;
        if (entry.supported_endpoint_types.some(endpoint => ["image-generation", "openai-video", "embeddings", "jina-rerank"].includes(endpoint))) continue;
        const endpoints = entry.supported_endpoint_types.filter(endpoint => Object.values(protocols).some(protocol => protocol.endpoint === endpoint));
        if (!endpoints.length) continue;
        const previous = catalog.get(entry.model_name);
        catalog.set(entry.model_name, {
          id: entry.model_name,
          endpoints: Array.from(new Set([...previous?.endpoints || [], ...endpoints]))
        });
      }
      const next = Array.from(catalog.values()).sort((a, b) => a.id.localeCompare(b.id, "en"));
      if (request.current !== controller) return;
      setModels(next);
      setSelected(current => current.filter(id => next.some(model => model.id === id && model.endpoints.includes(protocols[api].endpoint))));
    } catch {
      if (request.current === controller) setError(true);
    } finally {
      clearTimeout(timer);
      if (request.current === controller) {
        request.current = null;
        setLoading(false);
      }
    }
  };
  const copy = async value => {
    try {
      await navigator.clipboard.writeText(value);
      setFeedback(t.copied);
    } catch {
      setFeedback(t.copyError);
    }
  };
  const download = () => {
    const url = URL.createObjectURL(new Blob([configuration + "\n"], {
      type: "application/json"
    }));
    const link = document.createElement("a");
    link.href = url;
    link.download = "models.json";
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };
  return <div className="bt-pi-models not-prose">
      <div className="bt-pi-models-toolbar">
        <fieldset className="bt-pi-models-protocols" disabled={loading}>
          <legend>{t.api}</legend>
          <div className="bt-pi-models-options">
            {Object.entries(protocols).map(([value, protocol]) => <label key={value}>
                <input type="radio" name={protocolGroup} value={value} checked={api === value} onChange={() => {
    setApi(value);
    setSelected([]);
    setFeedback("");
  }} />
                <span>{protocol.label}</span>
              </label>)}
          </div>
        </fieldset>
        <button type="button" className="bt-pi-models-load" disabled={loading} onClick={loadModels}>
          <Icon icon={models === null ? "list" : "rotate-right"} size={16} />
          <span>{loading ? t.loading : models === null ? t.get : t.refresh}</span>
        </button>
      </div>
      {error && <p role="alert" className="bt-pi-models-error">{t.error} <a href={plazaUrl}>{t.plaza}</a></p>}
      {models !== null && <div>
          <label className="bt-pi-models-field bt-pi-models-search">
            <span>{t.search}</span>
            <input type="search" value={search} onChange={event => setSearch(event.target.value)} />
          </label>
          <p className="bt-pi-models-count" aria-live="polite">{t.models}: {visible.length} / {eligible.length} · {t.selected}: {chosen.length}</p>
          <div className="bt-pi-models-list">
            <table>
              <thead>
                <tr>
                  <th className="bt-pi-models-check">
                    <input type="checkbox" aria-label={t.selectAll} disabled={!visible.length} checked={visible.length > 0 && visible.every(model => selected.includes(model.id))} onChange={event => setSelected(current => event.target.checked ? Array.from(new Set([...current, ...visible.map(model => model.id)])) : current.filter(id => !visible.some(model => model.id === id)))} />
                  </th>
                  <th>Model ID</th>
                  <th className="bt-pi-models-copy" aria-label={t.copyId}></th>
                </tr>
              </thead>
              <tbody>
                {visible.map(model => <tr key={model.id}>
                    <td className="bt-pi-models-check">
                      <input type="checkbox" aria-label={t.select + " " + model.id} checked={selected.includes(model.id)} onChange={event => setSelected(current => event.target.checked ? [...current, model.id] : current.filter(id => id !== model.id))} />
                    </td>
                    <td><code>{model.id}</code></td>
                    <td className="bt-pi-models-copy">
                      <button type="button" className="bt-pi-models-icon" title={t.copyId} aria-label={t.copyId + " " + model.id} onClick={() => copy(model.id)}><Icon icon="copy" size={16} /></button>
                    </td>
                  </tr>)}
              </tbody>
            </table>
            {!visible.length && <p className="bt-pi-models-empty">{t.empty}</p>}
          </div>
          {chosen.length > 0 && <div className="bt-pi-models-config">
              <div className="bt-pi-models-config-header">
                <span>{t.config}</span>
                <div className="bt-pi-models-actions">
                  <button type="button" className="bt-pi-models-icon" title={t.copyConfig} aria-label={t.copyConfig} onClick={() => copy(configuration)}><Icon icon="copy" size={16} /></button>
                  <button type="button" className="bt-pi-models-icon" title={t.download} aria-label={t.download} onClick={download}><Icon icon="download" size={16} /></button>
                </div>
              </div>
              <pre aria-label={t.config}><code>{configuration}</code></pre>
            </div>}
        </div>}
      <p role="status" className="bt-pi-models-feedback">{feedback}</p>
    </div>;
};

Pi Agent, the Pi terminal coding agent, connects to compatible third-party APIs through `models.json`. This guide uses BetterToken: set an API key, match the Base URL to the protocol, and specify a Model ID. For another service, use its URL, credentials, and model IDs.

## Key settings

| Field | Value |
| - | - |
| API Key | BetterToken API Key through the `BETTERTOKEN_API_KEY` environment variable |
| Base URL | OpenAI protocol: `https://bettertoken.ai/v1`; Anthropic protocol: `https://bettertoken.ai` |
| Model | The exact Model ID from the <a href={"https://bettertoken.ai/pricing"}>model plaza</a> |
| Configuration file | `~/.pi/agent/models.json` |

## Prerequisites

* Node.js 22.19 or newer.
* <a href={"https://bettertoken.ai/register"}>Create a BetterToken API Key</a> and choose a model with tool calling from the model plaza.
* On native Windows, install [Git for Windows](https://git-scm.com/downloads/win) for Pi's Bash tool, or run Pi inside WSL.

## Install

Use Pi's current official npm installation command:

```bash theme={null}
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
pi --version
```

## Manual setup

### Set the API key

In the terminal that will start Pi, replace `YOUR_API_KEY` with your key:

<Tabs>
  <Tab title="macOS / Linux">
    ```bash theme={null}
    export BETTERTOKEN_API_KEY="YOUR_API_KEY"
    ```
  </Tab>

  <Tab title="Windows PowerShell">
    ```powershell theme={null}
    $env:BETTERTOKEN_API_KEY="YOUR_API_KEY"
    ```
  </Tab>
</Tabs>

### Get models and generate configuration

Click **Get model list** to fetch BetterToken's current public catalog without entering an API key. Choose an API and models with tool calling, then copy a Model ID or download a generated `models.json`.

If `models.json` already exists, merge the generated `bettertoken` entry into your current configuration instead of replacing the file. The configuration uses the `$BETTERTOKEN_API_KEY` variable set above; add any other model-specific settings you need manually.

Pi supports several protocols; choose one supported by both your model and endpoint. For this setup, we recommend **Anthropic Messages** for Claude, **Responses** for GPT when supported by the endpoint, and **Chat Completions** for other OpenAI-compatible models. The picker defaults to Chat Completions and filters by the APIs declared in the public catalog. If Responses does not list your model, use the manual example below for models with confirmed Responses support.

<PiModelPicker locale="en" plazaUrl="https://bettertoken.ai/pricing" />

### Add the URL and model

Create `~/.pi/agent/models.json`. On Windows, the default path is `%USERPROFILE%\.pi\agent\models.json`. If the file exists, merge the `bettertoken` entry into its existing `providers` object.

As an alternative to the generated configuration, fill in this Responses API example manually. Replace `YOUR_MODEL_ID` with a model plaza ID; keep `$BETTERTOKEN_API_KEY` as the environment-variable reference:

```json theme={null}
{
  "providers": {
    "bettertoken": {
      "baseUrl": "https://bettertoken.ai/v1",
      "api": "openai-responses",
      "apiKey": "$BETTERTOKEN_API_KEY",
      "models": [{ "id": "YOUR_MODEL_ID" }]
    }
  }
}
```

For another API, change both `baseUrl` and `api` using a matching pair:

| API | `api` | `baseUrl` |
| - | - | - |
| Responses API | `openai-responses` | `https://bettertoken.ai/v1` |
| Chat Completions | `openai-completions` | `https://bettertoken.ai/v1` |
| Anthropic Messages | `anthropic-messages` | `https://bettertoken.ai` |

Choose an API that supports tool calling for your model. For example, GPT-6.1 Sol uses Responses. Keep the Base URL as shown; Pi appends the request path.

### Start Pi

From your project directory, use the Model ID you configured:

```bash theme={null}
pi --provider bettertoken --model YOUR_MODEL_ID
```

Set the environment variable again in a new terminal, or persist it in your shell configuration.

## Verify the connection

Run `pi --list-models` and confirm that `bettertoken` and your model appear. Then start Pi and send:

```text theme={null}
Reply with PI_OK only. Do not use tools.
```

After receiving `PI_OK`, try a small read-only task to verify tool calling.

## Switch models

Add model entries to the `models` array, then run `/model` in Pi. Pi reloads `models.json`. Select a model and press `Ctrl+S` to save it as the default for new sessions. Use different local names for endpoints with different protocols, each with its own `api` and `baseUrl`.

## Common errors

| Symptom | What to do |
| - | - |
| Model missing from the list | Check JSON syntax, the Model ID, and the environment variable; reopen `/model`. |
| `401` or `Unauthorized` | Check that the variable contains a valid BetterToken API Key and Pi starts from the same terminal. |
| `404` or incorrect URL | Match `api` and `baseUrl` using the table. Remove manually appended `/responses`, `/chat/completions`, or `/v1/messages`. |
| Text works but tool calls fail | Check model and API tool support; use `openai-responses` for models that require Responses. |
| Bash missing on Windows | Install Git for Windows and reopen the terminal, or configure `shellPath` using Pi's Windows guide. |

## Advanced setup

For images, reasoning levels, or longer context, set `input`, `reasoning`, `contextWindow`, and `maxTokens` according to the model's actual capabilities. See the [model selection guide](/en/faq/model-calling/model-selection-guide).

## Technical details

`providers` and `--provider` are Pi client configuration fields; `bettertoken` is the local name used here. Pi checks a CLI `--api-key` first, then saved `auth.json` credentials, then `apiKey` in `models.json`. If that local name has old saved credentials, remove the corresponding entry through `/logout` in Pi before reconnecting.

### Official Pi references

* [Installation and quickstart](https://pi.dev/docs/latest/quickstart)
* [Custom URLs and models](https://pi.dev/docs/latest/models#configure-a-compatible-endpoint)
* [Command-line reference](https://pi.dev/docs/latest/cli)
* [Windows setup](https://pi.dev/docs/latest/windows)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.