pi-inferhub 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +136 -0
- package/README.md +117 -2
- package/docs/superpowers/plans/2026-10-06-inferhub-provider.md +438 -0
- package/docs/superpowers/specs/2026-10-06-inferhub-provider-design.md +207 -0
- package/package.json +28 -5
- package/src/auth.ts +48 -0
- package/src/catalog.ts +134 -0
- package/src/http.ts +100 -0
- package/src/index.ts +71 -0
- package/src/inferhub.ts +4 -0
- package/test/auth.test.ts +43 -0
- package/test/catalog.test.ts +74 -0
- package/test/http.test.ts +43 -0
- package/test/index.test.ts +159 -0
- package/tsconfig.json +14 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# pi-inferhub
|
|
2
|
+
|
|
3
|
+
pi provider extension exposing the user's InferHub (https://inferhub.dev)
|
|
4
|
+
**combos** as selectable chat models. Combos are the product: the extension
|
|
5
|
+
never surfaces `prefix/model` upstream ids, bare aliases, `free/` tier
|
|
6
|
+
models, or images/video generation. Combos are `combo/<slug>` models routed
|
|
7
|
+
through InferHub's OpenAI-compatible inference surface; all failover,
|
|
8
|
+
serialization, and selection happens server-side.
|
|
9
|
+
|
|
10
|
+
Spec (binding design authority): `docs/superpowers/specs/2026-10-06-inferhub-provider-design.md`
|
|
11
|
+
Plan: `docs/superpowers/plans/2026-10-06-inferhub-provider.md`
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
Four modules, acyclic, one-directional:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/inferhub.ts constants only — PROVIDER_NAME="inferhub", PROVIDER_DISPLAY_NAME="InferHub",
|
|
19
|
+
INFERENCE_BASE_URL="https://api.inferhub.dev/v1", MGMT_BASE_URL="https://inferhub.dev/api"
|
|
20
|
+
src/http.ts ← transport only — fetchJson (Bearer GET + typebox validation), InferhubHttpError{status,code,message}
|
|
21
|
+
src/auth.ts ← loginInferhub (prompt → validate GET /me → store), inferhubApiKeyAuth (envApiKeyAuth)
|
|
22
|
+
src/catalog.ts ← fetchComboCatalog (Promise.all of the two GETs), pure combosToModels for tests
|
|
23
|
+
src/index.ts ← default extension factory: createProvider registration + inferhub-login signpost command
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
No custom `streamSimple`. Everything streams through pi-ai's
|
|
27
|
+
`openAICompletionsApi()` (imported from `@earendil-works/pi-ai/compat` —
|
|
28
|
+
pi's extension loader does not virtualize lazy API subpaths). pi-ai owns
|
|
29
|
+
message conversion, tool handling, usage, cancellation, retries. Adding a
|
|
30
|
+
custom stream is a design-level decision, never a drive-by fix for an
|
|
31
|
+
upstream quirk.
|
|
32
|
+
|
|
33
|
+
## Wire contract (live-pinned 2026-10-07, verify before trusting)
|
|
34
|
+
|
|
35
|
+
- Auth: `Authorization: Bearer sk-airo-…` on BOTH surfaces.
|
|
36
|
+
- `GET MGMT_BASE_URL/combos` → **bare JSON array** of combos (id/name/slug/
|
|
37
|
+
members[]; member kinds `model` with `model: "prefix/upstream_id"` and
|
|
38
|
+
`alias` with bare name, no catalog linkage).
|
|
39
|
+
- `GET INFERENCE_BASE_URL/models` → `{object:"list", data:[...]}`; items
|
|
40
|
+
carry `id, modality ("text" | "text,image"), input_token_limit,
|
|
41
|
+
max_output_tokens, reasoning_levels, pricing.official_in/official_out`.
|
|
42
|
+
Combos do NOT appear here — that is why refresh fetches /combos
|
|
43
|
+
separately. `/v1/models` requires auth (401 unauthenticated).
|
|
44
|
+
- `GET MGMT_BASE_URL/me` → any JSON object; login validation endpoint.
|
|
45
|
+
- Management API rate limit 30 req/min/account; inference 500 req/min/account.
|
|
46
|
+
- Errors: `{"error":{"code","message"}}`; suspended/banned → 403
|
|
47
|
+
`account_blocked` with the ban reason in `message`.
|
|
48
|
+
- Cost: inference responses report `usage.cost` USD (OpenRouter convention),
|
|
49
|
+
$0.00001 minimum on billed requests. All management-surface money is USDC
|
|
50
|
+
decimal strings.
|
|
51
|
+
|
|
52
|
+
## Invariants (breaking any of these is a reverted commit)
|
|
53
|
+
|
|
54
|
+
1. **Reserved-key play-safe**: combo-level metadata (maxInputPerMtok/
|
|
55
|
+
maxOutputPerMtok caps, selection mode, filters, sticky, members) never
|
|
56
|
+
rides on the pi-ai `Model` object. Mapped models carry exactly pi-ai
|
|
57
|
+
`Model<"openai-completions">` fields.
|
|
58
|
+
2. **Cost honesty**: `cost.input/output` = per-direction MIN of resolvable
|
|
59
|
+
members' `official` rates (conservative upper bound — InferHub bills
|
|
60
|
+
discounts below official). `cost.cacheRead/cacheWrite` = 0 always —
|
|
61
|
+
InferHub's /v1/models pricing carries no cache fields; never fabricate.
|
|
62
|
+
3. **Fallback constants**: `CONTEXT_WINDOW_FALLBACK = 128_000`,
|
|
63
|
+
`MAX_TOKENS_FALLBACK = 8_192` — used when no resolvable member supplies
|
|
64
|
+
the field. pi-ai's `Model` type requires both as numbers; never guess a
|
|
65
|
+
model's ceiling other than via these constants.
|
|
66
|
+
4. **Image rule**: `input: ["text","image"]` only when ≥1 resolvable member
|
|
67
|
+
AND every resolvable member's `modality` includes `image`. Understating
|
|
68
|
+
input is acceptable; overstating sends requests the upstream rejects.
|
|
69
|
+
5. **Alias-member fallback**: empty resolvable set (alias-only combos, or
|
|
70
|
+
all member joins missed) → `input:["text"]`, `reasoning:true`, zero
|
|
71
|
+
cost, fallback limits. `reasoning:true` is deliberate: a false negative
|
|
72
|
+
hides thinking levels (the worse error); a false positive shows an inert
|
|
73
|
+
knob.
|
|
74
|
+
6. **thinkingLevelMap**: built from the union of resolvable members'
|
|
75
|
+
`reasoning_levels`; each pi level (`minimal|low|medium|high|xhigh|max`)
|
|
76
|
+
maps to itself when advertised, else `null`; `off:"off"`. Set only when
|
|
77
|
+
`reasoning` is true. Alias-only/level-less combos OMIT the map entirely —
|
|
78
|
+
an all-null map would hide every selectable level.
|
|
79
|
+
7. **No key leakage**: the API key appears ONLY in the Authorization header
|
|
80
|
+
and pi's credential store (`auth.json` via pi's own store). Never log,
|
|
81
|
+
print, or persist it anywhere else. Tests inject fetch mocks; a test
|
|
82
|
+
asserting the header is fine, echoing the key in fixtures is not.
|
|
83
|
+
8. **Unconfigured = zero models, no network**: `fetchModels` returns `[]`
|
|
84
|
+
and fetches nothing when auth resolves no key. pi-ai passes BOTH
|
|
85
|
+
stored-credential and env-resolved keys as `context.credential`
|
|
86
|
+
(verified in pi-ai 1.0.4 `dist/models.js` `resolveRefreshCredential`) —
|
|
87
|
+
do not add a second key-resolution path.
|
|
88
|
+
9. **No cross-run catalog persistence**: refresh is always live. A stale
|
|
89
|
+
combo snapshot silently serving the wrong routing is worse than an
|
|
90
|
+
explicit refresh.
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
npx vitest run # full suite (18 tests, <2s)
|
|
96
|
+
npx tsc --noEmit # typecheck
|
|
97
|
+
npm pack --dry-run # inspect package contents before publishing
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
pi packs the extension from `package.json` `pi.extensions:
|
|
101
|
+
["./src/index.ts"]`. Local dev: `pi -e <repo path>`. Published install:
|
|
102
|
+
`npm:pi-inferhub` in `~/.pi/agent/settings.json` packages (NOT yet
|
|
103
|
+
published as of 2026-10-07 — v0.1.0).
|
|
104
|
+
|
|
105
|
+
## Environment gotchas (learned the hard way)
|
|
106
|
+
|
|
107
|
+
- **TypeScript 6.0.3**: tsconfig MUST carry `types: ["node"]` — TS 6 dropped
|
|
108
|
+
automatic `@types/*` inclusion; without it `node:module`/`fetch`/`AbortSignal`
|
|
109
|
+
don't resolve and `tsc --noEmit` fails.
|
|
110
|
+
- **typebox 1.3.36**: compiled validators are typed `Validator<TProperties,
|
|
111
|
+
TSchema>` — pass the validator, not the schema, to `fetchJson`. Validation
|
|
112
|
+
error detail lives on `instancePath` (there is no `path` field). Extra
|
|
113
|
+
payload keys pass through by default; do NOT add
|
|
114
|
+
`additionalProperties: false` to wire schemas.
|
|
115
|
+
- **Git signing**: fresh clones inherit `gpg.format=ssh` globally but need
|
|
116
|
+
`git config user.signingkey ~/.ssh/michaelmacleod.id_ecdsa.pub` locally
|
|
117
|
+
before the first commit fails.
|
|
118
|
+
- **pi-ai `AuthEvent`** is a union; non-info variants carry no `message`.
|
|
119
|
+
Anything typing an interaction callback as `{type: string; message:
|
|
120
|
+
string}` breaks under contravariance — use `{type: string; message?:
|
|
121
|
+
string}`.
|
|
122
|
+
- **Live pinning of wire shapes is allowed and encouraged** when a shape is
|
|
123
|
+
undocumented: run a free listing GET, pin the parsed envelope in a typebox
|
|
124
|
+
validator, and leave a dated comment. Drift surfaces as
|
|
125
|
+
`code: "schema_mismatch"` with `instancePath` — fail-loud by design.
|
|
126
|
+
|
|
127
|
+
## Testing conventions
|
|
128
|
+
|
|
129
|
+
- Typebox schemas live at module scope (`Compile(Type.Object({...}))`).
|
|
130
|
+
- Tabs, ESM with `.js` import specifiers, single-responsibility modules.
|
|
131
|
+
- Tests verify real behavior through public seams (`combosToModels` pure
|
|
132
|
+
function; real `createProvider` refresh path in test/index.test.ts with
|
|
133
|
+
injected `fetchImpl`). No seams into internals.
|
|
134
|
+
- Deleting a brief-verbatim test line, or "fixing" a failing test by editing
|
|
135
|
+
the test to fit the code rather than the contract, is not allowed without
|
|
136
|
+
naming the contract being violated.
|
package/README.md
CHANGED
|
@@ -1,3 +1,118 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-inferhub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [Pi](https://pi.dev) extension that registers the
|
|
4
|
+
[InferHub](https://inferhub.dev) inference provider with Pi, exposing your
|
|
5
|
+
InferHub **combos** as selectable chat models.
|
|
6
|
+
|
|
7
|
+
InferHub pools AI subscriptions behind one OpenAI-compatible endpoint. Combos
|
|
8
|
+
are its server-side routed bundles — failover, cache-sticky, billed in USDC —
|
|
9
|
+
and they are the primary consumption surface. This extension exposes **combos
|
|
10
|
+
only**: each combo on your account appears as exactly one model named
|
|
11
|
+
`combo/<slug>`. Upstream `prefix/model` entries, bare-name aliases, and
|
|
12
|
+
free-tier models from InferHub's model list are intentionally not exposed.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Add the package to your Pi settings:
|
|
17
|
+
|
|
18
|
+
```jsonc
|
|
19
|
+
// ~/.pi/agent/settings.json
|
|
20
|
+
{
|
|
21
|
+
"packages": ["npm:pi-inferhub"]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
or run `pi install npm:pi-inferhub`.
|
|
26
|
+
|
|
27
|
+
For local development against a checkout:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pi -e /path/to/pi-inferhub
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Auth
|
|
34
|
+
|
|
35
|
+
Either:
|
|
36
|
+
|
|
37
|
+
- set `INFERHUB_API_KEY` in your environment (`sk-airo-…`), or
|
|
38
|
+
- run `/login inferhub` and paste the key at the prompt.
|
|
39
|
+
|
|
40
|
+
`/login inferhub` prompts for the key (input is masked), validates it against
|
|
41
|
+
`GET https://inferhub.dev/api/me`, and only then hands it to Pi's credential
|
|
42
|
+
store (`auth.json`). A rejected key (HTTP 401) notifies you and re-prompts;
|
|
43
|
+
after **three failed attempts** the login aborts without storing anything.
|
|
44
|
+
|
|
45
|
+
When both exist, the stored credential wins; `INFERHUB_API_KEY` is the
|
|
46
|
+
fallback when nothing is stored.
|
|
47
|
+
|
|
48
|
+
`/inferhub-login` reports whether the provider is currently configured and
|
|
49
|
+
points you to `/login inferhub` if not.
|
|
50
|
+
|
|
51
|
+
## Model selection
|
|
52
|
+
|
|
53
|
+
Send `/model` and filter by provider name `inferhub` or by `combo/`. Each
|
|
54
|
+
model id is `combo/<slug>` — the routable inference name on
|
|
55
|
+
`https://api.inferhub.dev/v1`.
|
|
56
|
+
|
|
57
|
+
## Catalog derivation
|
|
58
|
+
|
|
59
|
+
On refresh the extension makes two authenticated GETs —
|
|
60
|
+
`GET https://inferhub.dev/api/combos` (the account's combos) and
|
|
61
|
+
`GET https://api.inferhub.dev/v1/models` (per-model metadata) — and maps each
|
|
62
|
+
combo to one model:
|
|
63
|
+
|
|
64
|
+
- **Context/output limits** — minimum across resolvable members
|
|
65
|
+
(`input_token_limit` / `max_output_tokens`), so a combo is only as large as
|
|
66
|
+
its smallest member. When no resolvable member supplies limits, conservative
|
|
67
|
+
fallbacks apply: **128k context / 8k max output**.
|
|
68
|
+
- **Image input** — offered (`text` + `image`) only when *every* resolvable
|
|
69
|
+
member's modality includes image; otherwise text-only.
|
|
70
|
+
- **Reasoning** — the combo is marked reasoning-capable when any resolvable
|
|
71
|
+
member advertises `reasoning_levels`; Pi's thinking levels map onto the
|
|
72
|
+
advertised ladder (levels a combo doesn't advertise are unavailable).
|
|
73
|
+
- **Cost** — per-direction minimum of resolvable members' *official* rates
|
|
74
|
+
(see below). `cacheRead`/`cacheWrite` are shown as `0`: the catalog carries
|
|
75
|
+
no per-combo cache pricing, and none is fabricated.
|
|
76
|
+
|
|
77
|
+
Combos whose members are all unresolvable aliases get safe defaults:
|
|
78
|
+
text-only, reasoning-capable, `0` cost, and the fallback limits.
|
|
79
|
+
|
|
80
|
+
## Refresh semantics
|
|
81
|
+
|
|
82
|
+
The catalog is fetched **on demand** via the provider's `fetchModels`; the
|
|
83
|
+
extension keeps no snapshot of its own. Every refresh hits both endpoints
|
|
84
|
+
live, so combos added, removed, or changed server-side appear on the next
|
|
85
|
+
refresh instead of being served stale. When no credential is configured the
|
|
86
|
+
refresh is a no-op returning an empty list — no unauthenticated upstream call
|
|
87
|
+
and no silent fake catalog.
|
|
88
|
+
|
|
89
|
+
> [!NOTE]
|
|
90
|
+
> Pi refreshes provider model catalogs in TUI and RPC modes; other
|
|
91
|
+
> non-interactive modes use the latest persisted catalog. Open the TUI to
|
|
92
|
+
> discover new combos.
|
|
93
|
+
|
|
94
|
+
## Cost estimates
|
|
95
|
+
|
|
96
|
+
The per-model cost is an **upper-bound estimate**: the cheapest resolvable
|
|
97
|
+
member's *official* list rates (`official_in` / `official_out`) per direction.
|
|
98
|
+
InferHub's marketplace bills below list, so the real `usage.cost` reported per
|
|
99
|
+
request comes in **at or under** the displayed estimate.
|
|
100
|
+
|
|
101
|
+
## Design rationale
|
|
102
|
+
|
|
103
|
+
The full design — why combos only, why `/v1/models` supplies member metadata
|
|
104
|
+
instead of the management `/catalog` endpoint, why the OpenAI completions
|
|
105
|
+
surface, and the conservative-defaults philosophy — is in the
|
|
106
|
+
[design spec](docs/superpowers/specs/2026-10-06-inferhub-provider-design.md).
|
|
107
|
+
|
|
108
|
+
## Development
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npm install
|
|
112
|
+
npm test # vitest
|
|
113
|
+
npm run typecheck
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|