pi-airpx
v0.1.2
Published
Log in to the airpx LLM proxy (https://airpx.cc) with an sk-proxy API key and auto-import its model catalog (prices, context windows, reasoning, vision) into the pi coding agent.
Maintainers
Readme
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 — 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
Authorizationheader) 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
# ad-hoc
pi -e ./extensions/index.ts
# then inside pi:
/login airpx # paste your sk-proxy-... keyOr via env var (no /login needed):
AIRPX_API_KEY=sk-proxy-... pi -e ./extensions/index.tsAs an installed package — add to ~/.pi/agent/settings.json → packages:
{ "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.jsonairpxentry →AIRPX_API_KEY), fetches/v1/models(anonymously by default), maps →pi.registerProvider("airpx", { models }). /login airpxprompts 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, andAIRPX_ALL_MODELSnever 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
/modeluntil 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/modelsresponse and are never committed. Test fixtures deliberately use undiscountedpricing(not liveeffective_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 neitherpricingnorsupported_parameters, not bycontext_window === 0).
Tests
npm test
# = node --experimental-strip-types --testPure, 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.
TODO / open
- Map
thinkingLevelMapfromsupported_parameters(xhigh/max) per model. - Replace the
_OVERRIDEStable once the proxy exposesapi_formatper model. - Decide
openai-completionsvsanthropic-messagesper model family (currently all viaopenai-completions; the proxy normalizes).
