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 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
- # Temporary Holding Version
1
+ # pi-inferhub
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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