---
slug: "pi-airpx"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/axelbaumlisto/pi-airpx@master/README.md"
repo: "https://github.com/axelbaumlisto/pi-airpx"
source_file: "README.md"
branch: "master"
---
# pi-airpx

[![npm](https://img.shields.io/npm/v/pi-airpx)](https://www.npmjs.com/package/pi-airpx)

Log in to the **airpx LLM proxy** with an `sk-proxy-…` key and auto-import its
model catalog (prices, context windows, reasoning/vision flags) into the
[pi coding agent](https://github.com/earendil-works/pi-mono) — instead of
hand-maintaining hundreds of entries in `models.json`.

- **Proxy:** https://airpx.cc — OpenAI-compatible endpoint at `https://airpx.cc/v1`
- **Package:** https://www.npmjs.com/package/pi-airpx
- **Source:** https://github.com/axelbaumlisto/pi-airpx

The airpx proxy is an OpenAI-/Anthropic-compatible gateway that routes one
`sk-proxy-…` key to many upstream models (Claude, GPT, Gemini, Kimi, DeepSeek,
GLM, Qwen, Grok, …) with pooled keys, fallback routing, and unified pricing.
This plugin makes those models appear natively in pi's `/model` picker.

By default the plugin loads the proxy's **main list**: the anonymous
`GET https://airpx.cc/v1/models` response (`proxy_public_models`, the ~33
super-current canonical models), with pricing and any global discount already
applied. It skips alias rows and provider-group pseudo rows, and it does **not**
load free/service models beyond what the main list already exposes.

> The list is fetched **anonymously** (no `Authorization` header) on purpose: a
> key's role is derived from its owner's user role, so a normal user key may
> return the full ~348-model catalog. The curated main list is only obtainable
> anonymously.

## Install / run

```bash
# ad-hoc
pi -e ./extensions/index.ts
# then inside pi:
/login airpx        # paste your sk-proxy-... key
```

Or via env var (no `/login` needed):

```bash
AIRPX_API_KEY=sk-proxy-... pi -e ./extensions/index.ts
```

As an installed package — add to `~/.pi/agent/settings.json` → `packages`:

```json
{ "packages": ["npm:pi-airpx"] }
```

pi installs it from npm automatically. Then run `/login airpx` once and paste
your `sk-proxy-…` key (get one at https://airpx.cc).

### Get a key

Sign in at **https://airpx.cc**, create an `sk-proxy-…` API key, and use it with
`/login airpx` or `AIRPX_API_KEY`. The same key works with any OpenAI-compatible
client pointed at `https://airpx.cc/v1`.

## Config

| Env | Default | Meaning |
|-----|---------|---------|
| `AIRPX_API_KEY` | — | `sk-proxy-...` key (alternative to `/login airpx`) |
| `AIRPX_BASE_URL` | `https://airpx.cc/v1` | proxy base URL (use `http://127.0.0.1:18100/v1` for local dev) |
| `AIRPX_ALL_MODELS` | — | set to `1` to fetch the FULL keyed catalog (still skips alias/pseudo rows) |

## How it works

- **async factory** resolves a key (`auth.json` `airpx` entry → `AIRPX_API_KEY`),
  fetches `/v1/models` (anonymously by default), maps → `pi.registerProvider("airpx", { models })`.
- **`/login airpx`** prompts for the key, **validates it** with an explicit
  keyed probe (a bad/mistyped key is rejected, not silently saved), then
  fetches the default catalog and re-registers the provider live (no `/reload`).
- The on-disk cache (`~/.pi/agent/airpx-catalog.json`) is a **last-good
  snapshot**: a successful fetch **replaces** it with exactly the fresh
  response (so removed/re-priced models don't linger, and `AIRPX_ALL_MODELS`
  never permanently pollutes the default list); the cache is used only as a
  **fallback** when a fetch fails, so pi still boots offline.
- **First-run hint:** with no key configured the models load but pi keeps them
  unavailable in `/model` until auth is set (documented pi behavior), so the
  plugin prints a one-line hint pointing you to `/login airpx`.

## Field mapping (proxy → pi)

| proxy | pi |
|-------|----|
| `id` | `id` |
| `display_name` (else `id`) | `name` |
| `reasoning` | `reasoning` |
| `supports_vision` | `input: ["text","image"]` else `["text"]` |
| `context_window` (0 → 128000) | `contextWindow` |
| `max_output_tokens` (0 → 16384) | `maxTokens` |
| `effective_pricing`\|`pricing` `.input` | `cost.input` (0 if absent) |
| `…​.output` | `cost.output` (0 if absent) |
| `…​.cache_read` | `cost.cacheRead` (0 if absent) |
| `…​.cache_write` | `cost.cacheWrite` (0 if absent) |

A small hand-maintained `_OVERRIDES` table adds per-model tweaks (currently
`gpt-5.3-codex` → `api: "openai-responses"` and `kimi-for-coding` →
`name: "Kimi K2.6 Coding"`).

**Notes:**

- **Prices are runtime-only** — they come from the live `/v1/models` response
  and are **never committed**. Test fixtures deliberately use undiscounted
  `pricing` (not live `effective_pricing`) so they don't rot.
- **Only canonical ids are loaded** — alias rows (those carrying `alias_of`)
  are skipped, as are provider-group pseudo rows (e.g. `anthropic`, detected by
  a known provider name or by having neither `pricing` nor
  `supported_parameters`, **not** by `context_window === 0`).

## Tests

```bash
npm test
# = node --experimental-strip-types --test
```

Pure, offline unit tests of the mapper (`src/map.ts`) against a frozen fixture
(`test/fixtures/v1_models.sample.json`) — no network.

## Architecture

For the design rationale (module boundary, SOLID/DRY/KISS walkthrough, data
flow, failure model, extension points) see [docs/ARCHITECTURE.md](https://github.com/axelbaumlisto/pi-airpx/blob/HEAD/docs/ARCHITECTURE.md).

## TODO / open

- Map `thinkingLevelMap` from `supported_parameters` (xhigh/max) per model.
- Replace the `_OVERRIDES` table once the proxy exposes `api_format` per model.
- Decide `openai-completions` vs `anthropic-messages` per model family
  (currently all via `openai-completions`; the proxy normalizes).
