pi-inferhub 0.1.1 → 0.2.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 +23 -13
- package/package.json +1 -1
- package/pi-inferhub-0.2.0.tgz +0 -0
- package/src/catalog.ts +27 -6
- package/src/index.ts +1 -1
- package/test/catalog.test.ts +51 -2
package/AGENTS.md
CHANGED
|
@@ -59,18 +59,21 @@ upstream quirk.
|
|
|
59
59
|
members' `official` rates (conservative upper bound — InferHub bills
|
|
60
60
|
discounts below official). `cost.cacheRead/cacheWrite` = 0 always —
|
|
61
61
|
InferHub's /v1/models pricing carries no cache fields; never fabricate.
|
|
62
|
-
3. **Fallback constants**: `CONTEXT_WINDOW_FALLBACK =
|
|
63
|
-
`MAX_TOKENS_FALLBACK =
|
|
64
|
-
the field
|
|
65
|
-
|
|
62
|
+
3. **Fallback constants**: `CONTEXT_WINDOW_FALLBACK = 262_144`,
|
|
63
|
+
`MAX_TOKENS_FALLBACK = 131_072` — used when no resolvable member supplies
|
|
64
|
+
the field (last resort after the alias sibling join; see 5). pi-ai's
|
|
65
|
+
`Model` type requires both as numbers; never guess a model's ceiling
|
|
66
|
+
other than via these constants.
|
|
66
67
|
4. **Image rule**: `input: ["text","image"]` only when ≥1 resolvable member
|
|
67
68
|
AND every resolvable member's `modality` includes `image`. Understating
|
|
68
69
|
input is acceptable; overstating sends requests the upstream rejects.
|
|
69
|
-
5. **Alias-member fallback**:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
5. **Alias-member fallback**: bare alias rows (`owned_by: "alias"`, no
|
|
71
|
+
data — observed live 2026-10-15) and alias-kind members first resolve
|
|
72
|
+
through prefixed sibling servings of the same model name (`<prefix>/<name>`
|
|
73
|
+
rows in the /v1/models catalog, min-of applies). Only when no sibling
|
|
74
|
+
carries data either → `input:["text"]`, `reasoning:true`, zero cost,
|
|
75
|
+
fallback limits. `reasoning:true` is deliberate: a false negative hides
|
|
76
|
+
thinking levels (the worse error); a false positive shows an inert knob.
|
|
74
77
|
6. **thinkingLevelMap**: built from the union of resolvable members'
|
|
75
78
|
`reasoning_levels`; each pi level (`minimal|low|medium|high|xhigh|max`)
|
|
76
79
|
maps to itself when advertised, else `null`; `off:"off"`. Set only when
|
|
@@ -100,10 +103,20 @@ npm pack --dry-run # inspect package contents before publishing
|
|
|
100
103
|
pi packs the extension from `package.json` `pi.extensions:
|
|
101
104
|
["./src/index.ts"]`. Local dev: `pi -e <repo path>`. Published install:
|
|
102
105
|
`npm:pi-inferhub` in `~/.pi/agent/settings.json` packages (NOT yet
|
|
103
|
-
published
|
|
106
|
+
published npm:pi-inferhub (v0.1.1).
|
|
104
107
|
|
|
105
108
|
## Environment gotchas (learned the hard way)
|
|
106
109
|
|
|
110
|
+
- **Pi packages declare host modules as peerDependencies, never
|
|
111
|
+
dependencies**: `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`,
|
|
112
|
+
`typebox` (and pi-tui if used) go in `peerDependencies` with `"*"` ranges;
|
|
113
|
+
keep `dependencies` empty. Regular dependencies make pi's package installer
|
|
114
|
+
warn about duplicate host-module copies — extensions must bind to the
|
|
115
|
+
copies the pi host already loads. Mirror the same modules in
|
|
116
|
+
`devDependencies` (with real version pins) for the repo's own
|
|
117
|
+
tests/typecheck.
|
|
118
|
+
|
|
119
|
+
|
|
107
120
|
- **TypeScript 6.0.3**: tsconfig MUST carry `types: ["node"]` — TS 6 dropped
|
|
108
121
|
automatic `@types/*` inclusion; without it `node:module`/`fetch`/`AbortSignal`
|
|
109
122
|
don't resolve and `tsc --noEmit` fails.
|
|
@@ -112,9 +125,6 @@ published as of 2026-10-07 — v0.1.0).
|
|
|
112
125
|
error detail lives on `instancePath` (there is no `path` field). Extra
|
|
113
126
|
payload keys pass through by default; do NOT add
|
|
114
127
|
`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
128
|
- **pi-ai `AuthEvent`** is a union; non-info variants carry no `message`.
|
|
119
129
|
Anything typing an interaction callback as `{type: string; message:
|
|
120
130
|
string}` breaks under contravariance — use `{type: string; message?:
|
package/package.json
CHANGED
|
Binary file
|
package/src/catalog.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { INFERENCE_BASE_URL, MGMT_BASE_URL, PROVIDER_NAME } from "./inferhub.js"
|
|
|
7
7
|
/** pi thinking levels, in ladder order; `off` is handled separately. */
|
|
8
8
|
const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const satisfies readonly ThinkingLevel[];
|
|
9
9
|
|
|
10
|
-
const CONTEXT_WINDOW_FALLBACK =
|
|
11
|
-
const MAX_TOKENS_FALLBACK =
|
|
10
|
+
const CONTEXT_WINDOW_FALLBACK = 262_144;
|
|
11
|
+
const MAX_TOKENS_FALLBACK = 131_072;
|
|
12
12
|
|
|
13
13
|
export const MemberSchema = Type.Union([
|
|
14
14
|
Type.Object({ kind: Type.Literal("model"), model: Type.String() }),
|
|
@@ -24,8 +24,12 @@ export const ComboSchema = Type.Object({
|
|
|
24
24
|
});
|
|
25
25
|
export type Combo = Static<typeof ComboSchema>;
|
|
26
26
|
|
|
27
|
+
/** Live-pinned 2026-10-15: bare (prefix-less) catalog rows are aliases served by
|
|
28
|
+
* prefixed upstream rows — they carry owned_by "alias" and NO limits/pricing/
|
|
29
|
+
* reasoning fields at all. Unknown extra fields pass validation regardless. */
|
|
27
30
|
export const CatalogModelSchema = Type.Object({
|
|
28
31
|
id: Type.String(),
|
|
32
|
+
owned_by: Type.Optional(Type.String()),
|
|
29
33
|
modality: Type.Optional(Type.String()),
|
|
30
34
|
input_token_limit: Type.Optional(Type.Number()),
|
|
31
35
|
max_output_tokens: Type.Optional(Type.Number()),
|
|
@@ -34,6 +38,8 @@ export const CatalogModelSchema = Type.Object({
|
|
|
34
38
|
Type.Object({
|
|
35
39
|
official_in: Type.Number(),
|
|
36
40
|
official_out: Type.Number(),
|
|
41
|
+
min_ask_in: Type.Optional(Type.Number()),
|
|
42
|
+
min_ask_out: Type.Optional(Type.Number()),
|
|
37
43
|
}),
|
|
38
44
|
),
|
|
39
45
|
});
|
|
@@ -45,6 +51,10 @@ export type CatalogModel = Static<typeof CatalogModelSchema>;
|
|
|
45
51
|
* validation (no additionalProperties restrictions).
|
|
46
52
|
*/
|
|
47
53
|
const CombosEnvelope = Type.Array(ComboSchema);
|
|
54
|
+
/** Bare (prefix-less) alias row shape observed live 2026-10-15:
|
|
55
|
+
* {id, object:"model", created, owned_by:"alias", supports_cache} — no limits,
|
|
56
|
+
* pricing, modality, or reasoning fields at all. Prefixed servings of the same
|
|
57
|
+
* model carry the data; catalog.ts resolves aliases through them (sibling join). */
|
|
48
58
|
const ModelsEnvelope = Type.Object({ data: Type.Array(CatalogModelSchema) });
|
|
49
59
|
|
|
50
60
|
const CombosValidator = Compile(CombosEnvelope);
|
|
@@ -69,18 +79,29 @@ export function combosToModels(
|
|
|
69
79
|
catalogModels: CatalogModel[],
|
|
70
80
|
): Model<"openai-completions">[] {
|
|
71
81
|
const catalogById = new Map(catalogModels.map((model) => [model.id, model]));
|
|
72
|
-
return combos.map((combo) => comboToModel(combo, catalogById));
|
|
82
|
+
return combos.map((combo) => comboToModel(combo, catalogById, catalogModels));
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
function comboToModel(
|
|
76
86
|
combo: Combo,
|
|
77
87
|
catalogById: Map<string, CatalogModel>,
|
|
88
|
+
catalogModels: CatalogModel[],
|
|
78
89
|
): Model<"openai-completions"> {
|
|
90
|
+
// Alias rows (`owned_by: "alias"`, no data) and alias-kind members resolve
|
|
91
|
+
// through prefixed sibling servings of the same model name; min-of over the
|
|
92
|
+
// siblings bounds the combo exactly like directly-joined members.
|
|
79
93
|
const resolvable: CatalogModel[] = [];
|
|
80
94
|
for (const member of combo.members) {
|
|
81
|
-
|
|
82
|
-
const entry = catalogById.get(member.model);
|
|
83
|
-
if (entry !== undefined
|
|
95
|
+
const name = member.kind === "model" ? member.model : member.alias;
|
|
96
|
+
const entry = member.kind === "model" ? catalogById.get(member.model) : undefined;
|
|
97
|
+
if (entry !== undefined && entry.owned_by !== "alias") {
|
|
98
|
+
resolvable.push(entry);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const sibling of catalogModels) {
|
|
102
|
+
if (sibling.owned_by === "alias") continue;
|
|
103
|
+
if (sibling.id.endsWith(`/${name}`)) resolvable.push(sibling);
|
|
104
|
+
}
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
// Alias-only / all-missed-join combos reason optimistically: a false
|
package/src/index.ts
CHANGED
|
@@ -55,7 +55,7 @@ export function registerInferhubProvider(pi: InferhubRegistrationPi): void {
|
|
|
55
55
|
const status = ctx.modelRegistry.getProviderAuthStatus(PROVIDER_NAME);
|
|
56
56
|
if (status?.configured) {
|
|
57
57
|
const source = status.label ? ` (${status.label})` : "";
|
|
58
|
-
ctx.ui.notify(`InferHub is configured${source}. Run /
|
|
58
|
+
ctx.ui.notify(`InferHub is configured${source}. Run /model to pick a combo.`, "info");
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
ctx.ui.notify(
|
package/test/catalog.test.ts
CHANGED
|
@@ -20,6 +20,10 @@ const combo = (members: Combo["members"], overrides: Partial<Combo> = {}): Combo
|
|
|
20
20
|
id: "c1", name: "My Combo", slug: "my-combo", members, ...overrides,
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
/** Bare alias row shape observed live 2026-10-15: id without prefix, owned_by "alias",
|
|
24
|
+
* no limits/pricing/reasoning fields at all. */
|
|
25
|
+
const aliasRow = (name: string): CatalogModel => ({ id: name, owned_by: "alias" });
|
|
26
|
+
|
|
23
27
|
describe("combosToModels", () => {
|
|
24
28
|
it("maps id, name, provider, baseUrl, api", () => {
|
|
25
29
|
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }])], catalog);
|
|
@@ -49,8 +53,8 @@ describe("combosToModels", () => {
|
|
|
49
53
|
expect(aliasOnly[0].input).toEqual(["text"]);
|
|
50
54
|
expect(aliasOnly[0].reasoning).toBe(true);
|
|
51
55
|
expect(aliasOnly[0].cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
52
|
-
expect(aliasOnly[0].contextWindow).toBe(
|
|
53
|
-
expect(aliasOnly[0].maxTokens).toBe(
|
|
56
|
+
expect(aliasOnly[0].contextWindow).toBe(262_144);
|
|
57
|
+
expect(aliasOnly[0].maxTokens).toBe(131_072);
|
|
54
58
|
|
|
55
59
|
const partial = combosToModels([combo([{ kind: "model", model: "ghost/gone" }, { kind: "model", model: "croc/glm-5.3" }])], catalog);
|
|
56
60
|
expect(partial[0].contextWindow).toBe(200_000);
|
|
@@ -66,6 +70,51 @@ describe("combosToModels", () => {
|
|
|
66
70
|
expect(combosToModels([], catalog)).toEqual([]);
|
|
67
71
|
});
|
|
68
72
|
|
|
73
|
+
it("raises bare alias rows and alias-kind members via prefixed sibling join (live combo/fast shape)", () => {
|
|
74
|
+
// Members named like a user-built combo: bare names whose catalog rows are
|
|
75
|
+
// information-free alias rows, but prefixed servings of the same model exist.
|
|
76
|
+
const sibCatalog: CatalogModel[] = [
|
|
77
|
+
aliasRow("qwen3.8-flash"),
|
|
78
|
+
aliasRow("deepseek-v4.1-flash"),
|
|
79
|
+
aliasRow("glm-5.3-flash"),
|
|
80
|
+
{ id: "ocg/qwen3.8-flash", ...base },
|
|
81
|
+
{ id: "ali/qwen3.8-flash", ...base },
|
|
82
|
+
{ id: "ocg/deepseek-v4.1-flash", ...base, pricing: { official_in: 2.0, official_out: 10.0 } },
|
|
83
|
+
{ id: "zai/glm-5.3-flash", ...base },
|
|
84
|
+
];
|
|
85
|
+
const [m] = combosToModels([combo([
|
|
86
|
+
{ kind: "model", model: "qwen3.8-flash" },
|
|
87
|
+
{ kind: "alias", alias: "deepseek-v4.1-flash" },
|
|
88
|
+
{ kind: "model", model: "glm-5.3-flash" },
|
|
89
|
+
])], sibCatalog);
|
|
90
|
+
expect(m.contextWindow).toBe(200_000); // min over sibling servings, not the 128k fallback
|
|
91
|
+
expect(m.maxTokens).toBe(32_000);
|
|
92
|
+
expect(m.reasoning).toBe(true);
|
|
93
|
+
expect(m.thinkingLevelMap).toMatchObject({ low: "low", high: "high" });
|
|
94
|
+
expect(m.input).toEqual(["text", "image"]); // every sibling serving is vision-capable
|
|
95
|
+
expect(m.cost).toEqual({ input: 2.0, output: 10.0, cacheRead: 0, cacheWrite: 0 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("does not cross-join a siblingless alias and keeps safe defaults (claude fable shape)", () => {
|
|
99
|
+
// cc/ row exists but is null everywhere and has NO prefixed siblings carrying data.
|
|
100
|
+
const [m] = combosToModels([combo([{ kind: "model", model: "claude-fable-5-1" }])], [aliasRow("claude-fable-5-1")]);
|
|
101
|
+
expect(m.contextWindow).toBe(262_144);
|
|
102
|
+
expect(m.maxTokens).toBe(131_072);
|
|
103
|
+
expect(m.reasoning).toBe(true); // alias-only optimism invariant
|
|
104
|
+
expect(m.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("prefers a direct row's real data over sibling join", () => {
|
|
108
|
+
// A prefixed member resolves directly; siblings must not inflate its set.
|
|
109
|
+
const sibCatalog: CatalogModel[] = [
|
|
110
|
+
{ id: "croc/glm-5.3", ...base },
|
|
111
|
+
{ id: "other/glm-5.3", ...base, input_token_limit: 1_000_000, max_output_tokens: 128_000 },
|
|
112
|
+
];
|
|
113
|
+
const [m] = combosToModels([combo([{ kind: "model", model: "croc/glm-5.3" }])], sibCatalog);
|
|
114
|
+
expect(m.contextWindow).toBe(200_000); // only the directly-joined member counts
|
|
115
|
+
expect(m.maxTokens).toBe(32_000);
|
|
116
|
+
});
|
|
117
|
+
|
|
69
118
|
it("reasoning false when no member advertises levels", () => {
|
|
70
119
|
const noLevels: CatalogModel = { id: "x/plain", modality: "text" };
|
|
71
120
|
const [m] = combosToModels([combo([{ kind: "model", model: "x/plain" }])], [noLevels]);
|