@lokeraar/pi-enclave-bridge 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/donors.ts ADDED
@@ -0,0 +1,547 @@
1
+ /**
2
+ * donors.ts — where model values come from.
3
+ *
4
+ * A "donor" is a list of models whose values can be copied onto ours, matched on
5
+ * the BARE model name: the id with any `vendor/` prefix stripped on both sides,
6
+ * so `cyberouter/glm-5.3-flash` matches `glm-5.3-flash`. A prefix relationship is
7
+ * not an identity: `cyberouter/glm-5.3` does not inherit from `glm-5.3-flash`.
8
+ *
9
+ * ## One source, and it ships with Pi
10
+ *
11
+ * The donor is the catalog Pi bundles inside its own package:
12
+ *
13
+ * pi-ai/dist/providers/data/<provider>.json
14
+ *
15
+ * There is no hand-written donor and none is required. A list maintained by a
16
+ * person goes stale, and making every user own an account with an obscure
17
+ * provider before the extension does anything useful is a dependency nobody
18
+ * should have to accept. These files are already on disk after installing Pi,
19
+ * and reading a file needs no credential — a key is only needed to CALL an API,
20
+ * not to read what Pi shipped.
21
+ *
22
+ * openrouter the primary donor. It is the largest model router in the world
23
+ * and the catalog is its core business, so the numbers are kept
24
+ * by people who cannot afford to be wrong.
25
+ * the rest corroboration only. A provider lower in the order confirms the
26
+ * model exists and agrees on its structure; it fills a field the
27
+ * ones above left empty and never overrides.
28
+ *
29
+ * ## The order
30
+ *
31
+ * what is already in models.json > openrouter > the rest
32
+ *
33
+ * What is already written wins, because it is specific to this endpoint. Inside
34
+ * a 5% band the bundled figure takes over, since `128_000` and `131_072` are the
35
+ * same number written differently and the second is the exact one. Outside that
36
+ * band they genuinely disagree and what is written stands. Nothing is ever
37
+ * averaged: a number nobody published is not a consensus, it is an invention.
38
+ *
39
+ * ## What no donor may set
40
+ *
41
+ * contextWindow — the live catalog states what this endpoint actually serves
42
+ * cost — a donor's price is for a different reseller
43
+ * compat — OpenRouter's `thinkingFormat: "openrouter"` and friends
44
+ * describe how OpenRouter wants reasoning sent. EnClave speaks
45
+ * the OpenAI shape, which is how it was verified. Copying
46
+ * those flags would change the request format on an endpoint
47
+ * they were never tested against.
48
+ *
49
+ * ## Router aliases are never resolved
50
+ *
51
+ * `cyberouter/auto` is EnClave's own pseudo-model. OpenRouter has an `auto` too,
52
+ * advertising a 2,000,000 window — a different thing that happens to share the
53
+ * name, and a lie here.
54
+ */
55
+
56
+ import { readFileSync, readdirSync, statSync } from "node:fs";
57
+ import { dirname, join, resolve } from "node:path";
58
+ import { fileURLToPath } from "node:url";
59
+
60
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
61
+ export type ThinkingLevelMap = Partial<Record<ThinkingLevel, string | null>>;
62
+
63
+ export interface ModelEntry {
64
+ id: string;
65
+ name?: string;
66
+ api?: string;
67
+ provider?: string;
68
+ reasoning?: boolean;
69
+ thinkingLevelMap?: ThinkingLevelMap;
70
+ input?: Array<"text" | "image">;
71
+ cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
72
+ contextWindow?: number;
73
+ maxTokens?: number;
74
+ compat?: Record<string, unknown>;
75
+ /**
76
+ * Set by the sync script. `source` supplied the values; `corroborating`
77
+ * lists the other providers that also know the model and agree it exists.
78
+ */
79
+ donor?: {
80
+ /** The provider that supplied the values. */
81
+ source: string;
82
+ /**
83
+ * The exact id that matched, WITH its prefix — `qwen/qwen3.8-max-0902`, not
84
+ * `qwen3.8-max`. Matching is done on the bare name because the two sides
85
+ * carry different vendor prefixes, but recording the full id is what makes a
86
+ * match auditable: you can see which entry was taken, including when it came
87
+ * from a dated slug.
88
+ */
89
+ matchedId?: string;
90
+ /** Other catalogs that also know this model. They confirm, never override. */
91
+ corroborating: string[];
92
+ rule: "vendor" | "donated" | "kept" | "corroborated" | "none";
93
+ };
94
+ [key: string]: unknown;
95
+ }
96
+
97
+ /** Fields a bundled donor may contribute. `compat` is deliberately absent. */
98
+ export const BUNDLED_FIELDS = ["reasoning", "thinkingLevelMap", "input", "maxTokens"] as const;
99
+
100
+ /** Fields already written in models.json may contribute, `compat` included. */
101
+ export const HAND_FIELDS = [...BUNDLED_FIELDS, "compat"] as const;
102
+
103
+ /**
104
+ * How close a hand-written value must be to a bundled one to count as the same
105
+ * number. `128_000` vs `131_072` is 2.3%; `232_000` vs `384_000` is 66%.
106
+ */
107
+ export const ROUNDING_TOLERANCE = 0.05;
108
+
109
+ export function bareName(id: string): string {
110
+ const i = id.lastIndexOf("/");
111
+ return i === -1 ? id : id.slice(i + 1);
112
+ }
113
+
114
+ /**
115
+ * EnClave sells a model under a short name; a catalog lists the same weights
116
+ * under the dated slug the vendor publishes. These are the same model, so the
117
+ * dated slug is looked up when the short one finds nothing.
118
+ *
119
+ * Kept as data, not a rule: each line is a fact about one model, not a general
120
+ * "strip the date" heuristic. Stripping dates automatically would be wrong —
121
+ * `deepseek-v4-flash` and `deepseek-v4-flash-0731` are different checkpoints,
122
+ * and so is `qwen3.8-max` from `qwen3.8-max-0902`.
123
+ */
124
+ export const NAME_ALIASES: Record<string, readonly string[]> = {
125
+ "qwen3.8-max": ["qwen3.8-max-0902"],
126
+ "nemotron-ultra": ["nemotron-3-ultra-550b-a55b"],
127
+ };
128
+
129
+ function num(v: unknown): number | undefined {
130
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Where Pi keeps its bundled catalogs
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Find `pi-ai/dist/providers/data` without hardcoding the version.
139
+ *
140
+ * The directory is named `@earendil-works+pi-ai@<version>_<dependency hash>`,
141
+ * so the hash changes on every Pi update and any stored path dies with it. It
142
+ * sits under the pnpm store beneath the agent directory; this walks that store
143
+ * rather than guessing a depth.
144
+ */
145
+ export interface CatalogLocation {
146
+ dir: string;
147
+ /** Which copy this is, for the report. */
148
+ origin: "pi install" | "global install";
149
+ }
150
+
151
+ function catalogAt(piAiPackage: string): string | undefined {
152
+ const candidate = join(dirname(piAiPackage), "dist", "providers", "data");
153
+ try {
154
+ if (statSync(candidate).isDirectory()) return candidate;
155
+ } catch {
156
+ return undefined;
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ /**
162
+ * Where the Pi package itself may live. Only used to resolve pi-ai.
163
+ *
164
+ * The prefix is derived from `process.execPath` rather than assumed: on Termux
165
+ * it is `/data/data/com.termux/files/usr`, not `/usr`, so a hardcoded `/usr/lib`
166
+ * silently never matches. Getting this wrong does not throw — it just sends the
167
+ * lookup to the wrong copy of the catalog.
168
+ */
169
+ function piPackageCandidates(): string[] {
170
+ const out: string[] = [];
171
+ const add = (base: string) => {
172
+ const p = resolve(base, "@earendil-works", "pi-coding-agent");
173
+ try {
174
+ if (statSync(p).isDirectory()) out.push(p);
175
+ } catch {
176
+ // not here
177
+ }
178
+ };
179
+
180
+ // The install this module lives under, when Pi loads it from its own bundle.
181
+ add(dirname(fileURLToPath(import.meta.url)) + "/..");
182
+
183
+ // The global prefix this node was installed under.
184
+ try {
185
+ add(dirname(dirname(process.execPath)) + "/lib/node_modules");
186
+ } catch {
187
+ // execPath unavailable
188
+ }
189
+
190
+ // Conventional prefixes, only used where they actually exist.
191
+ add("/usr/local/lib/node_modules");
192
+ add("/usr/lib/node_modules");
193
+
194
+ return [...new Set(out)];
195
+ }
196
+
197
+ /**
198
+ * Find Pi's bundled catalogs without depending on a version or a hash.
199
+ *
200
+ * The directory is named `@earendil-works+pi-ai@<version>_<dependency hash>`, so
201
+ * any stored path dies on the next update. Only stable prefixes are used and the
202
+ * tree is searched at run time.
203
+ *
204
+ * Order matters, because there can be more than one copy of pi-ai and reading
205
+ * the wrong one fails silently:
206
+ *
207
+ * 1. ASK NODE. `createRequire` from Pi's own package resolves the exact pi-ai
208
+ * that Pi loads its models from. That is ground truth, not a guess.
209
+ * 2. FALL BACK to the agent's pnpm store, which is where `pi install npm:…`
210
+ * puts things on this device. When several versions sit there, the one whose
211
+ * version matches Pi's package wins; otherwise the highest, never a `+` build.
212
+ *
213
+ * On this device step 1 currently fails: the global install of pi-ai is an empty
214
+ * directory, so Pi resolves it from the agent's store. Both steps are kept
215
+ * because either can be true after an update.
216
+ */
217
+ export function findBundledCatalogs(agentDir: string): CatalogLocation[] {
218
+ const found: CatalogLocation[] = [];
219
+ const push = (dir: string | undefined, origin: CatalogLocation["origin"]) => {
220
+ if (dir && !found.some((f) => f.dir === dir)) found.push({ dir, origin });
221
+ };
222
+
223
+ // Walking the tree by hand rather than using module resolution: pi-ai is
224
+ // ESM-only with an `exports` map that exposes neither a main nor its own
225
+ // package.json, so require.resolve fails on it with
226
+ // ERR_PACKAGE_PATH_NOT_EXPORTED. The directory is right there next to Pi, so
227
+ // looking for it directly is both simpler and immune to that.
228
+ for (const piPkg of piPackageCandidates()) {
229
+ push(catalogAt(join(piPkg, "node_modules", "@earendil-works", "pi-ai", "package.json")), "la que usa Pi");
230
+ }
231
+
232
+ for (const dir of storeCatalogs(agentDir)) push(dir, "pi install");
233
+ return found;
234
+ }
235
+
236
+ /**
237
+ * Every usable copy in the agent's pnpm store, best first.
238
+ *
239
+ * When several versions sit there the newest wins, and a `+` build never does —
240
+ * a prerelease is never the one a released Pi runs. Exported separately so the
241
+ * preference order can be tested without the Pi install shadowing it.
242
+ */
243
+ export function storeCatalogs(agentDir: string): string[] {
244
+ const store = join(agentDir, "npm", "node_modules", ".pnpm");
245
+ let entries: string[] = [];
246
+ try {
247
+ entries = readdirSync(store).filter((e) => e.startsWith("@earendil-works+pi-ai@"));
248
+ } catch {
249
+ return [];
250
+ }
251
+ const out: string[] = [];
252
+ entries
253
+ .map((e) => ({ entry: e, version: e.slice("@earendil-works+pi-ai@".length).split("_")[0] }))
254
+ .filter((c) => !c.version.includes("+"))
255
+ .sort((a, b) => compareVersions(b.version, a.version))
256
+ .forEach((c) => {
257
+ const dir = catalogAt(join(store, c.entry, "node_modules", "@earendil-works", "pi-ai", "package.json"));
258
+ if (dir) out.push(dir);
259
+ });
260
+ return out;
261
+ }
262
+
263
+ /** Pi's own version, for the report only. Note this is NOT pi-ai's version:
264
+ * the two use independent numbering (1.0.0 vs 0.85.1), so they never match and
265
+ * must not be compared. */
266
+ export function piPackageVersion(): string | undefined {
267
+ for (const piPkg of piPackageCandidates()) {
268
+ try {
269
+ const pkg = JSON.parse(readFileSync(join(piPkg, "package.json"), "utf8")) as { version?: string };
270
+ if (pkg.version) return pkg.version;
271
+ } catch {
272
+ // try the next
273
+ }
274
+ }
275
+ return undefined;
276
+ }
277
+
278
+ /** Numeric dotted comparison, so 0.9 sorts below 0.10. */
279
+ function compareVersions(a: string, b: string): number {
280
+ const pa = a.split(".").map((n) => Number(n) || 0);
281
+ const pb = b.split(".").map((n) => Number(n) || 0);
282
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
283
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
284
+ if (d) return d;
285
+ }
286
+ return 0;
287
+ }
288
+
289
+ /** The catalogs to read, best location first. Never throws. */
290
+ export function findBundledCatalogDir(agentDir: string): string | undefined {
291
+ return findBundledCatalogs(agentDir)[0]?.dir;
292
+ }
293
+
294
+ /** Provider ids that have a credential in `auth.json`. */
295
+ export function activeProviders(agentDir: string): Set<string> {
296
+ try {
297
+ const auth = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8")) as Record<string, unknown>;
298
+ return new Set(Object.keys(auth));
299
+ } catch {
300
+ return new Set();
301
+ }
302
+ }
303
+
304
+ export interface BundledCatalog {
305
+ provider: string;
306
+ /** bare name -> entry */
307
+ models: Map<string, ModelEntry>;
308
+ }
309
+
310
+ /** Read one bundled catalog. The JSON is keyed by API, then by model id. */
311
+ export function readBundledCatalog(
312
+ catalogDir: string,
313
+ provider: string,
314
+ ): BundledCatalog | undefined {
315
+ let raw: Record<string, Record<string, ModelEntry>>;
316
+ try {
317
+ raw = JSON.parse(readFileSync(join(catalogDir, `${provider}.json`), "utf8"));
318
+ } catch {
319
+ return undefined;
320
+ }
321
+ const models = new Map<string, ModelEntry>();
322
+ for (const byApi of Object.values(raw)) {
323
+ if (!byApi || typeof byApi !== "object") continue;
324
+ for (const entry of Object.values(byApi)) {
325
+ if (!entry || typeof entry.id !== "string") continue;
326
+ // "free" models are deliberately excluded: they routinely ship with
327
+ // capabilities cut down, so their numbers describe a reduced product.
328
+ if (/free$/i.test(entry.id)) continue;
329
+ const bare = bareName(entry.id);
330
+ if (!models.has(bare)) models.set(bare, entry);
331
+ }
332
+ }
333
+ return { provider, models };
334
+ }
335
+
336
+ /** Every bundled catalog belonging to an active provider, free ids removed. */
337
+ /**
338
+ * Every catalog Pi ships, best donor first.
339
+ *
340
+ * No credential is required and none is asked for: these files are data Pi
341
+ * already installed. OpenRouter leads because it is the primary donor; the rest
342
+ * are corroboration.
343
+ */
344
+ export function readPiCatalogs(agentDir: string, options: { exclude?: readonly string[] } = {}): BundledCatalog[] {
345
+ const dir = findBundledCatalogDir(agentDir);
346
+ if (!dir) return [];
347
+ const skip = new Set(options.exclude ?? []);
348
+ let files: string[] = [];
349
+ try {
350
+ files = readdirSync(dir).filter((f) => f.endsWith(".json"));
351
+ } catch {
352
+ return [];
353
+ }
354
+ const out: BundledCatalog[] = [];
355
+ for (const file of files) {
356
+ const provider = file.slice(0, -".json".length);
357
+ if (skip.has(provider)) continue;
358
+ const catalog = readBundledCatalog(dir, provider);
359
+ if (catalog && catalog.models.size) out.push(catalog);
360
+ }
361
+ const rank = (p: string) => {
362
+ const i = PROVIDER_PRIORITY.indexOf(p as (typeof PROVIDER_PRIORITY)[number]);
363
+ return i === -1 ? PROVIDER_PRIORITY.length : i;
364
+ };
365
+ return out.sort((a, b) => rank(a.provider) - rank(b.provider) || a.provider.localeCompare(b.provider));
366
+ }
367
+
368
+ /**
369
+ ): BundledCatalog[] {
370
+ const dir = findBundledCatalogDir(agentDir);
371
+ if (!dir) return [];
372
+ const skip = new Set(options.exclude ?? []);
373
+ const out: BundledCatalog[] = [];
374
+ for (const provider of activeProviders(agentDir)) {
375
+ if (skip.has(provider)) continue;
376
+ const catalog = readBundledCatalog(dir, provider);
377
+ if (catalog && catalog.models.size) out.push(catalog);
378
+ }
379
+ // Strict order: the first provider that knows a model supplies its values.
380
+ // The hand-written layer is passed separately and always outranks these.
381
+ const rank = (p: string) => {
382
+ const i = PROVIDER_PRIORITY.indexOf(p as (typeof PROVIDER_PRIORITY)[number]);
383
+ return i === -1 ? PROVIDER_PRIORITY.length : i;
384
+ };
385
+ return out.sort((a, b) => rank(a.provider) - rank(b.provider) || a.provider.localeCompare(b.provider));
386
+ }
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // Resolving one model
390
+ // ---------------------------------------------------------------------------
391
+
392
+ /**
393
+ * Which provider's catalog outranks which. Anything not listed falls after
394
+ * these, in the order they appear in auth.json.
395
+ */
396
+ export const PROVIDER_PRIORITY = ["openrouter"] as const;
397
+
398
+ /**
399
+ * The vendor's own model card, where it is known and a catalog is wrong.
400
+ *
401
+ * This outranks every catalog, including OpenRouter. The reasoning is specific:
402
+ * a catalog records what a RESELLER believes a model accepts, while the model
403
+ * card records what the model itself implements. When those disagree the
404
+ * catalog is usually not lying — it is describing the gateway's shape. EnClave
405
+ * accepts all six effort values for `glm-5.3`; the card says the model only
406
+ * implements low, high and max. The extra values are accepted and then ignored,
407
+ * which is worse than not offering them: Pi would show a thinking level that
408
+ * silently does nothing.
409
+ *
410
+ * Every entry here is a transcription of a published card, not an inference, and
411
+ * carries the reason it exists. `null` in a thinking map means the level is not
412
+ * supported; `off: null` means thinking cannot be switched off.
413
+ */
414
+ export interface VendorSpec {
415
+ input?: Array<"text" | "image">;
416
+ maxTokens?: number;
417
+ thinkingLevelMap?: Record<string, string | null>;
418
+ /** Why this entry exists, so a future reader can check it. */
419
+ why: string;
420
+ }
421
+
422
+ export const VENDOR_SPEC: Record<string, VendorSpec> = {
423
+ "glm-5.3": {
424
+ input: ["text"],
425
+ maxTokens: 131_072,
426
+ thinkingLevelMap: { off: null, minimal: null, low: "low", medium: null, high: "high", xhigh: null, max: "max" },
427
+ why: "GLM-5.3 model card: no vision/audio/video, reasoning low|high|max, context 1M, max output 128K. OpenRouter declares 943718 and all six levels; the endpoint accepts both, and simply ignores the levels the card does not list.",
428
+ },
429
+ "glm-5.2": {
430
+ input: ["text"],
431
+ maxTokens: 131_072,
432
+ thinkingLevelMap: { off: null, minimal: null, low: null, medium: null, high: "high", xhigh: null, max: "max" },
433
+ why: "GLM-5.2 model card: no vision/audio/video, reasoning high|max, context 1M, max output 128K. OpenRouter declares 943718 and all six levels; same reason as glm-5.3.",
434
+ },
435
+ };
436
+
437
+ export interface Resolved {
438
+ entry: ModelEntry;
439
+ /** The source that supplied the values, if any. */
440
+ source?: string;
441
+ /** Every other provider that also knows this model: corroboration only. */
442
+ corroborating: string[];
443
+ rule: "vendor" | "donated" | "kept" | "corroborated" | "none";
444
+ /** The exact id in the donor catalog that matched, prefix included. */
445
+ matchedId?: string;
446
+ }
447
+
448
+ const withinTolerance = (a: number, b: number) =>
449
+ Math.abs(a - b) <= ROUNDING_TOLERANCE * Math.max(Math.abs(a), Math.abs(b));
450
+
451
+ /**
452
+ * Resolve one model's values, in strict priority order:
453
+ *
454
+ * hand-written > openrouter > the rest of the active providers
455
+ *
456
+ * The first source that knows the model supplies its values. Every other source
457
+ * that knows it is recorded as corroboration — it confirms the model exists and
458
+ * agrees on its structure, but it never overrides a higher source. Nothing is
459
+ * averaged.
460
+ */
461
+ export function resolveModel(
462
+ bare: string,
463
+ kept: ModelEntry | undefined,
464
+ bundled: readonly BundledCatalog[],
465
+ isAlias: boolean,
466
+ ): Resolved {
467
+ // Aliases are left alone: the same bare name is a different thing in a
468
+ // different router, and its catalog numbers would be false here.
469
+ if (isAlias) return { entry: {}, corroborating: [], rule: "none" };
470
+
471
+ // The exact name first; a dated vendor slug only when the catalog does not
472
+ // know the short one.
473
+ const candidates = [bare, ...(NAME_ALIASES[bare] ?? [])];
474
+ const knowing = bundled
475
+ .map((c) => {
476
+ for (const name of candidates) {
477
+ const entry = c.models.get(name);
478
+ if (entry) return { provider: c.provider, entry, matchedId: entry.id };
479
+ }
480
+ return undefined;
481
+ })
482
+ .filter((h): h is { provider: string; entry: ModelEntry; matchedId: string } => h !== undefined);
483
+
484
+ // The primary donor is whichever catalog ranks first in PROVIDER_PRIORITY.
485
+ const primary = bundled.length ? bundled[0].provider : undefined;
486
+ const donor = knowing.find((h) => h.provider === primary) ?? knowing[0];
487
+ const corroborating = knowing.filter((h) => h.provider !== donor?.provider).map((h) => h.provider);
488
+
489
+ // 1. The vendor's own card, where one exists. Nothing outranks the model
490
+ // card about the model.
491
+ const spec = VENDOR_SPEC[bare];
492
+ if (spec) {
493
+ const entry: ModelEntry = donor ? copyFields(donor.entry, BUNDLED_FIELDS) : {};
494
+ if (spec.input) entry.input = spec.input;
495
+ if (spec.maxTokens !== undefined) entry.maxTokens = spec.maxTokens;
496
+ if (spec.thinkingLevelMap) entry.thinkingLevelMap = spec.thinkingLevelMap as ThinkingLevelMap;
497
+ if (!donor && kept) for (const f of HAND_FIELDS) if (kept[f] !== undefined) (entry as Record<string, unknown>)[f] = kept[f];
498
+ return {
499
+ entry,
500
+ source: "model card",
501
+ matchedId: donor?.matchedId,
502
+ corroborating: corroborating.concat(donor ? [donor.provider] : []),
503
+ rule: "vendor",
504
+ };
505
+ }
506
+
507
+ // 2. The primary donor decides every field it knows. It is a catalog whose
508
+ // whole business is routing these models.
509
+ if (donor) {
510
+ return {
511
+ entry: copyFields(donor.entry, BUNDLED_FIELDS),
512
+ source: donor.provider,
513
+ matchedId: donor.matchedId,
514
+ corroborating,
515
+ rule: corroborating.length ? "corroborated" : "donated",
516
+ };
517
+ }
518
+
519
+ // 3. No donor knows this model: what is already written stands. It may be a
520
+ // value measured against this endpoint, which no catalog can beat.
521
+ if (kept) {
522
+ return {
523
+ entry: copyFields(kept, HAND_FIELDS),
524
+ source: "models.json",
525
+ corroborating: [],
526
+ rule: "kept",
527
+ };
528
+ }
529
+
530
+ return { entry: {}, corroborating: [], rule: "none" };
531
+ }
532
+
533
+ function copyFields(from: ModelEntry, fields: readonly string[]): ModelEntry {
534
+ const out: ModelEntry = {};
535
+ for (const field of fields) {
536
+ if (from[field] !== undefined) (out as Record<string, unknown>)[field] = from[field];
537
+ }
538
+ return out;
539
+ }
540
+
541
+ /**
542
+ * Pi's extension loader treats EVERY `extensions/*.ts` file as an extension
543
+ * factory and reports "does not export a valid factory function" otherwise.
544
+ * This file is a helper imported by `index.ts`, so it needs a no-op default
545
+ * export to load silently beside it.
546
+ */
547
+ export default async function donorsHelper(): Promise<void> {}