> ## 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 외부 API 연결: Base URL, API Key, 모델 설정

> Pi Agent의 models.json에 외부 API의 Base URL, API Key, Model ID를 설정하세요. Responses, Chat Completions, Anthropic 프로토콜 선택과 연결 확인, 모델 전환, 오류 해결을 설명합니다.

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는 터미널 코딩 에이전트 Pi입니다. `models.json`으로 호환되는 외부 API에 연결합니다. BetterToken을 예로 API Key, 프로토콜에 맞는 Base URL, Model ID를 설정합니다. 다른 서비스를 연결하려면 해당 서비스의 URL, 인증 정보, 모델 ID를 사용하세요.

## 주요 설정

| 항목 | 값 |
| - | - |
| API Key | `BETTERTOKEN_API_KEY` 환경 변수로 전달하는 BetterToken API Key |
| Base URL | OpenAI 프로토콜: `https://bettertoken.ai/v1`; Anthropic 프로토콜: `https://bettertoken.ai` |
| Model | <a href={"https://bettertoken.ai/pricing"}>model plaza</a>의 정확한 Model ID |
| 설정 파일 | `~/.pi/agent/models.json` |

## 사전 요구 사항

* Node.js 22.19 이상.
* <a href={"https://bettertoken.ai/register"}>BetterToken API Key를 만들고</a> model plaza에서 도구 호출을 지원하는 모델을 선택하세요.
* 네이티브 Windows에서는 Pi의 Bash 도구를 위해 [Git for Windows](https://git-scm.com/downloads/win)를 설치하거나 WSL에서 Pi를 실행하세요.

## 설치

Pi의 현재 공식 npm 명령을 실행하세요.

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

## 수동 설정

### API Key 설정

Pi를 실행할 터미널에서 `YOUR_API_KEY`를 자신의 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>

### 모델을 가져와 설정 생성하기

**모델 목록 가져오기**를 클릭하면 API Key를 입력하지 않고 BetterToken의 최신 공개 모델 카탈로그를 가져올 수 있습니다. API와 도구 호출을 지원하는 모델을 선택한 후 Model ID를 복사하거나 생성된 `models.json`을 다운로드하세요.

기존 `models.json`이 있다면 파일 전체를 덮어쓰지 말고 생성된 `bettertoken` 항목을 기존 설정에 병합하세요. 설정은 위에서 지정한 `$BETTERTOKEN_API_KEY` 변수를 사용합니다. 추가 매개변수는 모델의 실제 기능에 맞게 설정하세요.

Pi는 여러 프로토콜을 지원하므로 모델과 서버가 지원하는 프로토콜을 선택하세요. 이 연결에서는 Claude에 **Anthropic Messages**, 서버가 지원하는 경우 GPT에 **Responses**, 다른 OpenAI 호환 모델에 **Chat Completions**를 권장합니다. 선택기의 기본값은 Chat Completions이며 공개 카탈로그에 명시된 API로 목록을 필터링합니다. Responses에 원하는 모델이 없으면 Responses 지원이 확인된 모델에 대해 아래 수동 설정 예시를 사용하세요.

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

### URL과 모델 추가

`~/.pi/agent/models.json`을 만드세요. Windows 기본 경로는 `%USERPROFILE%\.pi\agent\models.json`입니다. 기존 파일이 있으면 `providers` 객체에 `bettertoken` 항목을 추가하세요.

생성된 설정을 사용하지 않는 경우 아래 Responses API 예시를 직접 작성할 수도 있습니다. `YOUR_MODEL_ID`를 model plaza의 ID로 바꾸고, 환경 변수 참조인 `$BETTERTOKEN_API_KEY`는 그대로 유지하세요.

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

다른 API를 쓰려면 `baseUrl`과 `api`를 함께 바꾸세요.

| 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` |

모델의 도구 호출을 지원하는 API를 선택하세요. 예를 들어 GPT-6.1 Sol은 Responses를 사용합니다. Base URL은 표의 값으로 유지하면 Pi가 요청 경로를 붙입니다.

### Pi 시작

프로젝트 폴더에서 설정한 Model ID로 실행하세요.

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

새 터미널에서는 환경 변수를 다시 설정하거나 셸 설정에 저장하세요.

## 연결 확인

`pi --list-models`를 실행해 `bettertoken`과 모델이 표시되는지 확인하세요. 그다음 Pi를 시작하고 다음 메시지를 보내세요.

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

`PI_OK`를 받으면 작은 읽기 전용 작업으로 도구 호출도 확인하세요.

## 모델 전환

`models` 배열에 모델을 추가한 뒤 Pi에서 `/model`을 실행하면 `models.json`을 다시 읽습니다. 모델을 선택하고 `Ctrl+S`를 누르면 새 세션의 기본 모델로 저장됩니다. 프로토콜이 다른 연결은 로컬 이름을 구분하고 각각 `api`와 `baseUrl`을 지정하세요.

## 자주 발생하는 오류

| 증상 | 해결 방법 |
| - | - |
| 모델이 목록에 없음 | JSON 형식, Model ID, 환경 변수를 확인하고 `/model`을 다시 여세요. |
| `401` 또는 `Unauthorized` | 변수의 BetterToken API Key가 유효한지, 같은 터미널에서 Pi를 실행했는지 확인하세요. |
| `404` 또는 잘못된 URL | 표에 맞게 `api`와 `baseUrl`을 확인하세요. 직접 추가한 `/responses`, `/chat/completions`, `/v1/messages`를 제거하세요. |
| 텍스트는 되지만 도구 호출이 실패함 | 모델과 API의 도구 지원을 확인하고, Responses가 필요한 모델에는 `openai-responses`를 사용하세요. |
| Windows에서 Bash를 찾지 못함 | Git for Windows를 설치하고 터미널을 다시 열거나 Pi Windows 가이드에 따라 `shellPath`를 설정하세요. |

## 고급 설정

이미지, 추론 수준, 긴 컨텍스트가 필요하면 실제 모델 기능에 맞게 `input`, `reasoning`, `contextWindow`, `maxTokens`를 설정하세요. [모델 선택 가이드](/ko/faq/model-calling/model-selection-guide)를 참고하세요.

## 기술 세부 정보

`providers`와 `--provider`는 Pi 클라이언트의 설정 필드이며 `bettertoken`은 이 예시의 로컬 이름입니다. Pi는 `--api-key`, `auth.json`의 저장된 인증 정보, `models.json`의 `apiKey` 순서로 사용합니다. 같은 이름에 이전 인증 정보가 저장되어 있다면 Pi의 `/logout`으로 해당 항목을 제거한 뒤 다시 연결하세요.

### Pi 공식 자료

* [설치 및 빠른 시작](https://pi.dev/docs/latest/quickstart)
* [사용자 지정 URL 및 모델](https://pi.dev/docs/latest/models#configure-a-compatible-endpoint)
* [명령줄 참조](https://pi.dev/docs/latest/cli)
* [Windows 설정](https://pi.dev/docs/latest/windows)


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