# 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
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.

Model **ids and names are the proxy's, verbatim** — the plugin keeps no rename or
relabel tables, so pi's picker shows exactly what the airpx landing page lists.
If a model is missing from pi, it is missing from `proxy_public_models`; publish
it on the server rather than patching the client.

> 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 catalog instead. 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` (preferred — stored in `auth.json` with `0600`) or, for headless
setups, `AIRPX_API_KEY`. The same key works with any OpenAI-compatible client
pointed at `https://airpx.cc/v1`.

## Config

Every environment variable is optional — with a key saved by `/login airpx` the
plugin needs none of them. The plugin registers no CLI flags of its own; use the
standard `pi --model airpx/<id>`.

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

**Credential precedence: `auth.json` wins over the environment.** The stored
`airpx` entry is read first and `AIRPX_API_KEY` is only a fallback, so exporting
the variable while a credential is saved changes nothing — log out or edit
`~/.pi/agent/auth.json` to switch keys.

Store the key in `auth.json` (created `0600` by `/login`) rather than exporting
it from your shell: `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` pointed at the proxy
are a common shortcut that backfires — pi then registers its own built-in
`anthropic` and `openai` providers from those names, and the same proxy shows up
a second time in `/model` under a different name and with pi's model metadata
instead of the catalog's.

## 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 plus `apiFromCatalog()` add per-model
tweaks. GPT-5.x rows default to `api: "openai-responses"` so Pi renders native
Responses reasoning modules; explicit proxy fields `recommended_api`/`api_format`
win once airpx exposes them. `kimi-for-coding` is renamed to `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](docs/ARCHITECTURE.md).

## TODO / open

- Map `thinkingLevelMap` from `supported_parameters` (xhigh/max) per model.
- Replace the transitional GPT-5.x fallback in `apiFromCatalog()` once the proxy exposes `recommended_api`/`api_format` per model.
- Decide `openai-completions` vs `anthropic-messages` per model family
  (currently all via `openai-completions`; the proxy normalizes).
