@mzwing/pi-model-info 0.1.0 → 0.1.1
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/README.md +42 -19
- package/dist/index.js +357 -86
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,8 +23,9 @@ This extension looks each model up in two catalogs and fills the gaps at runtime
|
|
|
23
23
|
## What it does not do
|
|
24
24
|
|
|
25
25
|
It never creates providers, never discovers models, never changes `id` / `provider` / `baseUrl` /
|
|
26
|
-
`api` / auth / transport, and never writes to any of your files.
|
|
27
|
-
|
|
26
|
+
`api` / auth / transport, and never writes to any of your files. Where it wraps a provider it
|
|
27
|
+
delegates that provider's own auth, transport and refresh behaviour untouched. Remove it and Pi
|
|
28
|
+
goes back to exactly what it did before.
|
|
28
29
|
|
|
29
30
|
## Install
|
|
30
31
|
|
|
@@ -39,6 +40,26 @@ Then opt a provider in — nothing happens until you do:
|
|
|
39
40
|
{ "providers": { "my-relay": {} } }
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
## Staying current
|
|
44
|
+
|
|
45
|
+
Model lists move. Pi refreshes every built-in provider's catalog from pi.dev on a four-hour clock
|
|
46
|
+
and again the moment you open `/model`, and a discovery extension may refetch its own list. A
|
|
47
|
+
completion that was snapshotted once does not survive any of that, so it is applied in whichever of
|
|
48
|
+
three ways claims the least:
|
|
49
|
+
|
|
50
|
+
| What Pi already holds for the provider | How it is completed | How current the list stays |
|
|
51
|
+
| ------------------------------------------------------ | --------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
52
|
+
| Nothing else registered, and it refreshes its own list | the provider object is wrapped; `getModels()` completes on read | live — a model that appears mid-session is completed on first read |
|
|
53
|
+
| A sibling extension registered `refreshModels` | that hook is wrapped, so each refresh returns a completed list | live — `/model` renders the completion in the refresh that fetched it |
|
|
54
|
+
| Anything else | the list is snapshotted and re-registered | fixed for the session, re-checked on each agent turn |
|
|
55
|
+
|
|
56
|
+
`/model-info` names which one is in force, per provider.
|
|
57
|
+
|
|
58
|
+
Only the third row can freeze a dynamic list, and it is the only one that warns. It is reached when
|
|
59
|
+
something else owns the provider's registration slot — Pi merges every extension registration for a
|
|
60
|
+
provider into one entry, and taking that entry over would take the sibling's `apiKey` with it — or
|
|
61
|
+
when models.json defines the provider's `models[]`, which Pi rebuilds above anything underneath.
|
|
62
|
+
|
|
42
63
|
## Pairing with a discovery extension
|
|
43
64
|
|
|
44
65
|
If your provider's models come from something like
|
|
@@ -49,9 +70,10 @@ completes _what they are_.**
|
|
|
49
70
|
|
|
50
71
|
Ordering is guaranteed, not lucky. Discovery extensions register from inside their factory, which
|
|
51
72
|
Pi flushes before any session event; this extension registers from `session_start`, strictly
|
|
52
|
-
after. It only ever sends
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
after. It only ever sends `models` — plus, for a sibling that refreshes, a wrapper around that
|
|
74
|
+
sibling's own `refreshModels` — so Pi's merge preserves its `baseUrl`, `api`, and `apiKey`, and it
|
|
75
|
+
never calls `unregisterProvider` on a registration carrying keys it did not write, because that
|
|
76
|
+
call would take those credentials with it.
|
|
55
77
|
|
|
56
78
|
Where the sibling already supplied a real value and the catalogs cannot resolve the model, its
|
|
57
79
|
value is kept byte-for-byte. Where you prefer its numbers even when a catalog does resolve, set
|
|
@@ -101,17 +123,17 @@ Global at `~/.pi/agent/extensions/pi-model-info/config.json`, per-project at
|
|
|
101
123
|
|
|
102
124
|
### Per provider
|
|
103
125
|
|
|
104
|
-
| Key | Default | Meaning
|
|
105
|
-
| --------------------- | --------- |
|
|
106
|
-
| `catalogProvider` | — | Scope lookups to one catalog provider, e.g. `openrouter`.
|
|
107
|
-
| `costMultiplier` | `1` | Relay markup, applied to catalog pricing only — never to a rule's explicit `0`.
|
|
108
|
-
| `costPolicy` | `catalog` | `zero` to force free, `keep` to leave pricing alone.
|
|
109
|
-
| `contextWindowPolicy` | `catalog` | `min` never raises the limit past what Pi already had; `keep` leaves limits alone.
|
|
110
|
-
| `capabilityPolicy` | `catalog` | `widen` only ever adds a capability; `keep` leaves them alone.
|
|
111
|
-
| `useCatalogName` | `false` | Rename models to their catalog names.
|
|
112
|
-
| `mapThinkingLevels` | `false` | Derive a thinking-level map from models.dev. pi.dev's real map is always used.
|
|
113
|
-
| `allowDynamic` | `false` | Suppress the warning
|
|
114
|
-
| `models` | — | Per-model gates: `alias`, `override`, `skip`, `prefixes`, `suffixes`.
|
|
126
|
+
| Key | Default | Meaning |
|
|
127
|
+
| --------------------- | --------- | ------------------------------------------------------------------------------------- |
|
|
128
|
+
| `catalogProvider` | — | Scope lookups to one catalog provider, e.g. `openrouter`. |
|
|
129
|
+
| `costMultiplier` | `1` | Relay markup, applied to catalog pricing only — never to a rule's explicit `0`. |
|
|
130
|
+
| `costPolicy` | `catalog` | `zero` to force free, `keep` to leave pricing alone. |
|
|
131
|
+
| `contextWindowPolicy` | `catalog` | `min` never raises the limit past what Pi already had; `keep` leaves limits alone. |
|
|
132
|
+
| `capabilityPolicy` | `catalog` | `widen` only ever adds a capability; `keep` leaves them alone. |
|
|
133
|
+
| `useCatalogName` | `false` | Rename models to their catalog names. |
|
|
134
|
+
| `mapThinkingLevels` | `false` | Derive a thinking-level map from models.dev. pi.dev's real map is always used. |
|
|
135
|
+
| `allowDynamic` | `false` | Suppress the warning when a refreshing provider can only be completed by replacement. |
|
|
136
|
+
| `models` | — | Per-model gates: `alias`, `override`, `skip`, `prefixes`, `suffixes`. |
|
|
115
137
|
|
|
116
138
|
`contextWindowPolicy: "min"` and `capabilityPolicy: "keep"` exist because inflating a limit or
|
|
117
139
|
promoting a capability the relay does not actually support turns into a failed request rather
|
|
@@ -176,9 +198,10 @@ cost: $0/$0 per Mtok from rule 'free-dash'
|
|
|
176
198
|
|
|
177
199
|
## Limits
|
|
178
200
|
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
|
|
201
|
+
- A provider whose registration slot is already taken, or whose `models[]` models.json spells out,
|
|
202
|
+
can only be completed by replacement, which fixes its list for the session. It warns when it sees
|
|
203
|
+
one; a mid-session change is picked up on the next agent turn. See
|
|
204
|
+
[Staying current](#staying-current).
|
|
182
205
|
- If two extensions complete the same provider, the last one to register wins.
|
|
183
206
|
- With no cache and no network, nothing is applied at all rather than partially.
|
|
184
207
|
|
package/dist/index.js
CHANGED
|
@@ -863,6 +863,12 @@ var CatalogStore = class {
|
|
|
863
863
|
//#endregion
|
|
864
864
|
//#region src/command.ts
|
|
865
865
|
const COMPLETION_LIMIT = 50;
|
|
866
|
+
/** How current each provider's list stays — the one thing you cannot tell from the counts. */
|
|
867
|
+
const STRATEGY_NOTE = {
|
|
868
|
+
native: "list re-read live",
|
|
869
|
+
decorate: "list re-read on refresh",
|
|
870
|
+
replace: "list fixed for this session"
|
|
871
|
+
};
|
|
866
872
|
function splitReference(reference) {
|
|
867
873
|
const separator = reference.indexOf("/");
|
|
868
874
|
return separator > 0 ? {
|
|
@@ -896,7 +902,8 @@ function formatSummary(reports, catalog, issues, now) {
|
|
|
896
902
|
continue;
|
|
897
903
|
}
|
|
898
904
|
const counts = countByKind(report.models);
|
|
899
|
-
|
|
905
|
+
const note = report.strategy === void 0 ? "" : ` · ${STRATEGY_NOTE[report.strategy]}`;
|
|
906
|
+
lines.push(`${report.provider}: ${counts.resolved} completed, ${counts.ambiguous} ambiguous, ${counts.unresolved} unresolved (${report.models.length} models)${note}`);
|
|
900
907
|
}
|
|
901
908
|
if (catalog === void 0) lines.push("", "catalogs: not loaded yet");
|
|
902
909
|
else {
|
|
@@ -1055,8 +1062,7 @@ function readUserAuthoredFields(agentDir, readFile = defaultRead) {
|
|
|
1055
1062
|
const models = /* @__PURE__ */ new Map();
|
|
1056
1063
|
for (const definition of provider["models"]) {
|
|
1057
1064
|
if (!isRecord(definition) || typeof definition["id"] !== "string") continue;
|
|
1058
|
-
|
|
1059
|
-
if (fields.size > 0) models.set(definition["id"], fields);
|
|
1065
|
+
models.set(definition["id"], new Set(Object.keys(definition).filter((key) => TRACKED_FIELDS.has(key))));
|
|
1060
1066
|
}
|
|
1061
1067
|
if (models.size > 0) authored.set(providerId, models);
|
|
1062
1068
|
}
|
|
@@ -1448,137 +1454,220 @@ function resolveModel(input) {
|
|
|
1448
1454
|
};
|
|
1449
1455
|
}
|
|
1450
1456
|
|
|
1457
|
+
//#endregion
|
|
1458
|
+
//#region src/complete.ts
|
|
1459
|
+
/** The single completion pass, shared by every strategy so they cannot drift apart. */
|
|
1460
|
+
function completeModel(snapshot, context) {
|
|
1461
|
+
const resolution = resolveModel({
|
|
1462
|
+
index: context.catalog.index,
|
|
1463
|
+
provider: context.provider,
|
|
1464
|
+
prefixRules: context.config.prefixRules,
|
|
1465
|
+
suffixRules: context.config.suffixRules,
|
|
1466
|
+
modelId: snapshot.id
|
|
1467
|
+
});
|
|
1468
|
+
const merged = mergeMetadata({
|
|
1469
|
+
snapshot,
|
|
1470
|
+
provider: context.provider,
|
|
1471
|
+
resolution,
|
|
1472
|
+
gate: context.provider.models.get(snapshot.id),
|
|
1473
|
+
userAuthored: context.authored?.get(snapshot.id)
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
model: merged.model,
|
|
1477
|
+
report: {
|
|
1478
|
+
id: snapshot.id,
|
|
1479
|
+
resolution,
|
|
1480
|
+
provenance: merged.provenance,
|
|
1481
|
+
issues: merged.issues,
|
|
1482
|
+
model: merged.model
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
//#endregion
|
|
1488
|
+
//#region src/native-wrap.ts
|
|
1489
|
+
/**
|
|
1490
|
+
* Registered globally so a `/reload` — which hands us a fresh module instance — still recognises
|
|
1491
|
+
* our own wrapper and unwraps to the provider underneath instead of completing a completion.
|
|
1492
|
+
*/
|
|
1493
|
+
const WRAPPED = Symbol.for("@mzwing/pi-model-info.wrapped-provider");
|
|
1494
|
+
function isOurWrapper(provider) {
|
|
1495
|
+
return provider !== void 0 && WRAPPED in provider;
|
|
1496
|
+
}
|
|
1497
|
+
function unwrapProvider(provider) {
|
|
1498
|
+
return provider[WRAPPED] ?? provider;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* A provider whose `getModels()` completes the list underneath on every read.
|
|
1502
|
+
*
|
|
1503
|
+
* This is what keeps a built-in provider reactive: Pi's own dynamic providers keep their list in a
|
|
1504
|
+
* closure that `refreshModels()` rewrites, so reading it late — rather than snapshotting it once and
|
|
1505
|
+
* registering a replacement list — is what lets `/model`, the four-hourly pi.dev catalog refresh and
|
|
1506
|
+
* anything else that refreshes show newly discovered models already completed.
|
|
1507
|
+
*
|
|
1508
|
+
* The wrapper is a spread of the provider it wraps, which is safe only because we never wrap a
|
|
1509
|
+
* third-party native registration: Pi's own providers are closures over their state, never `this`.
|
|
1510
|
+
*/
|
|
1511
|
+
var NativeWrap = class {
|
|
1512
|
+
pristine;
|
|
1513
|
+
provider;
|
|
1514
|
+
deps;
|
|
1515
|
+
cache = /* @__PURE__ */ new WeakMap();
|
|
1516
|
+
constructor(pristine, deps) {
|
|
1517
|
+
this.pristine = pristine;
|
|
1518
|
+
this.deps = deps;
|
|
1519
|
+
const wrapper = {
|
|
1520
|
+
...pristine,
|
|
1521
|
+
[WRAPPED]: pristine,
|
|
1522
|
+
getModels: () => this.completeAll()
|
|
1523
|
+
};
|
|
1524
|
+
this.provider = wrapper;
|
|
1525
|
+
}
|
|
1526
|
+
/** Drops the per-model cache; call whenever the catalog or the resolved config changes. */
|
|
1527
|
+
invalidate() {
|
|
1528
|
+
this.cache = /* @__PURE__ */ new WeakMap();
|
|
1529
|
+
}
|
|
1530
|
+
completeAll() {
|
|
1531
|
+
const base = this.pristine.getModels();
|
|
1532
|
+
const context = this.deps.context();
|
|
1533
|
+
if (context === void 0) return base;
|
|
1534
|
+
try {
|
|
1535
|
+
const reports = [];
|
|
1536
|
+
const models = base.map((model) => {
|
|
1537
|
+
let completed = this.cache.get(model);
|
|
1538
|
+
if (completed === void 0) {
|
|
1539
|
+
const completion = completeModel(model, context);
|
|
1540
|
+
completed = {
|
|
1541
|
+
model: {
|
|
1542
|
+
...model,
|
|
1543
|
+
...completion.model
|
|
1544
|
+
},
|
|
1545
|
+
report: completion.report
|
|
1546
|
+
};
|
|
1547
|
+
this.cache.set(model, completed);
|
|
1548
|
+
}
|
|
1549
|
+
reports.push(completed.report);
|
|
1550
|
+
return completed.model;
|
|
1551
|
+
});
|
|
1552
|
+
this.deps.onReports(reports);
|
|
1553
|
+
return models;
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
this.deps.warn(`completing '${this.pristine.id}' failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1556
|
+
return base;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1451
1561
|
//#endregion
|
|
1452
1562
|
//#region src/provider-apply.ts
|
|
1563
|
+
/** Global, like the provider marker: a `/reload` must not let us decorate our own decorator. */
|
|
1564
|
+
const DECORATED = Symbol.for("@mzwing/pi-model-info.refresh-decorator");
|
|
1453
1565
|
var ProviderApplier = class {
|
|
1454
|
-
|
|
1566
|
+
tracked = /* @__PURE__ */ new Map();
|
|
1455
1567
|
skipped = /* @__PURE__ */ new Map();
|
|
1456
1568
|
reports = /* @__PURE__ */ new Map();
|
|
1457
1569
|
lastRegistered = /* @__PURE__ */ new Map();
|
|
1570
|
+
/** Outlives `capture`, so a `/reload` reuses one wrapper per provider instead of nesting them. */
|
|
1571
|
+
wraps = /* @__PURE__ */ new Map();
|
|
1572
|
+
/** Provider to the catalog fingerprint it was last registered with, to skip a no-op re-register. */
|
|
1573
|
+
appliedCatalog = /* @__PURE__ */ new Map();
|
|
1458
1574
|
warn;
|
|
1575
|
+
applied;
|
|
1459
1576
|
constructor(deps = {}) {
|
|
1460
1577
|
this.warn = deps.warn ?? (() => {});
|
|
1461
1578
|
}
|
|
1462
1579
|
/**
|
|
1463
|
-
*
|
|
1464
|
-
* returns our own
|
|
1580
|
+
* Chooses a strategy per provider, and for the two registration strategies takes the list as Pi
|
|
1581
|
+
* has it now: afterwards `getProvider(id).getModels()` returns our own, and re-deriving from it
|
|
1582
|
+
* would fold every previous pass into the next one.
|
|
1465
1583
|
*/
|
|
1466
|
-
capture(registry, config) {
|
|
1467
|
-
this.
|
|
1584
|
+
capture(registry, config, authored) {
|
|
1585
|
+
this.tracked.clear();
|
|
1468
1586
|
this.skipped.clear();
|
|
1469
1587
|
this.reports.clear();
|
|
1588
|
+
this.appliedCatalog.clear();
|
|
1470
1589
|
for (const provider of config.providers.values()) {
|
|
1471
1590
|
const live = registry.getProvider(provider.id);
|
|
1472
1591
|
if (live === void 0) {
|
|
1473
1592
|
this.skip(provider.id, "not present in Pi; check the provider id");
|
|
1474
1593
|
continue;
|
|
1475
1594
|
}
|
|
1476
|
-
|
|
1595
|
+
const native = registry.getRegisteredNativeProvider(provider.id);
|
|
1596
|
+
if (native !== void 0 && !isOurWrapper(native)) {
|
|
1477
1597
|
this.skip(provider.id, "another extension registered a native provider for this id");
|
|
1478
1598
|
continue;
|
|
1479
1599
|
}
|
|
1480
|
-
const
|
|
1600
|
+
const pristine = native === void 0 ? live : unwrapProvider(native);
|
|
1601
|
+
const registered = registry.getRegisteredProviderConfig(provider.id);
|
|
1602
|
+
if (registered === void 0 && pristine.refreshModels !== void 0 && !authored.has(provider.id)) {
|
|
1603
|
+
this.trackNative(provider, pristine);
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
const snapshot = [...pristine.getModels()];
|
|
1481
1607
|
if (snapshot.length === 0) {
|
|
1482
1608
|
this.skip(provider.id, "no models to complete");
|
|
1483
1609
|
continue;
|
|
1484
1610
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1611
|
+
const sibling = undecorate(registered?.refreshModels);
|
|
1612
|
+
if (sibling !== void 0) {
|
|
1613
|
+
this.track(provider, "decorate", snapshot, {
|
|
1614
|
+
refresh: this.decorate(provider.id, sibling),
|
|
1615
|
+
api: registered?.api,
|
|
1616
|
+
baseUrl: registered?.baseUrl
|
|
1617
|
+
});
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
if (pristine.refreshModels !== void 0 && !provider.allowDynamic) this.warn(`provider '${provider.id}' refreshes its model list dynamically, but ${registered === void 0 ? "models.json defines models for it" : "another extension owns its registration"}, so completing it freezes newly discovered models until the next session`);
|
|
1621
|
+
this.track(provider, "replace", snapshot, {});
|
|
1493
1622
|
}
|
|
1494
1623
|
}
|
|
1495
1624
|
apply(pi, config, catalog, userAuthored) {
|
|
1496
1625
|
if (catalog.status === "unavailable") return;
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
const models = [];
|
|
1505
|
-
const reports = [];
|
|
1506
|
-
for (const snapshot of snapshots) {
|
|
1507
|
-
const resolution = resolveModel({
|
|
1508
|
-
index: catalog.index,
|
|
1509
|
-
provider,
|
|
1510
|
-
prefixRules: config.prefixRules,
|
|
1511
|
-
suffixRules: config.suffixRules,
|
|
1512
|
-
modelId: snapshot.id
|
|
1513
|
-
});
|
|
1514
|
-
const merged = mergeMetadata({
|
|
1515
|
-
snapshot,
|
|
1516
|
-
provider,
|
|
1517
|
-
resolution,
|
|
1518
|
-
gate: provider.models.get(snapshot.id),
|
|
1519
|
-
userAuthored: authored?.get(snapshot.id)
|
|
1520
|
-
});
|
|
1521
|
-
models.push(merged.model);
|
|
1522
|
-
reports.push({
|
|
1523
|
-
id: snapshot.id,
|
|
1524
|
-
resolution,
|
|
1525
|
-
provenance: merged.provenance,
|
|
1526
|
-
issues: merged.issues,
|
|
1527
|
-
model: merged.model
|
|
1528
|
-
});
|
|
1529
|
-
}
|
|
1530
|
-
try {
|
|
1531
|
-
pi.registerProvider(provider.id, { models });
|
|
1532
|
-
this.lastRegistered.set(provider.id, models);
|
|
1533
|
-
this.reports.set(provider.id, {
|
|
1534
|
-
provider: provider.id,
|
|
1535
|
-
status: "applied",
|
|
1536
|
-
reason: void 0,
|
|
1537
|
-
models: reports
|
|
1538
|
-
});
|
|
1539
|
-
} catch (error) {
|
|
1540
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1541
|
-
this.warn(`failed to complete provider '${provider.id}': ${message}`);
|
|
1542
|
-
this.reports.set(provider.id, {
|
|
1543
|
-
provider: provider.id,
|
|
1544
|
-
status: "failed",
|
|
1545
|
-
reason: message,
|
|
1546
|
-
models: reports
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1626
|
+
this.applied = {
|
|
1627
|
+
config,
|
|
1628
|
+
catalog,
|
|
1629
|
+
authored: userAuthored
|
|
1630
|
+
};
|
|
1631
|
+
for (const [providerId, tracked] of this.tracked) if (tracked.strategy === "native") this.applyNative(pi, providerId);
|
|
1632
|
+
else this.applyRegistration(pi, providerId, tracked);
|
|
1549
1633
|
}
|
|
1550
1634
|
/**
|
|
1551
|
-
* Cheap id-set comparison against the live list
|
|
1552
|
-
*
|
|
1635
|
+
* Cheap id-set comparison against the live list, for the strategies that snapshot one. `native`
|
|
1636
|
+
* re-reads on every access and `decorate` completes inside the refresh, so neither can drift.
|
|
1553
1637
|
*/
|
|
1554
1638
|
reconcile(registry) {
|
|
1555
1639
|
let drifted = false;
|
|
1556
|
-
for (const providerId of this.
|
|
1640
|
+
for (const [providerId, tracked] of this.tracked) {
|
|
1641
|
+
if (tracked.strategy === "native") continue;
|
|
1557
1642
|
const live = registry.getProvider(providerId);
|
|
1558
1643
|
if (live === void 0) continue;
|
|
1559
1644
|
const liveModels = [...live.getModels()];
|
|
1560
1645
|
const registered = this.lastRegistered.get(providerId);
|
|
1561
1646
|
if (liveModels.length === 0 || registered !== void 0 && sameIds(liveModels, registered)) continue;
|
|
1562
|
-
|
|
1647
|
+
tracked.snapshot = liveModels;
|
|
1563
1648
|
drifted = true;
|
|
1564
1649
|
}
|
|
1565
1650
|
return drifted;
|
|
1566
1651
|
}
|
|
1567
1652
|
/**
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1653
|
+
* Registrations outlive a `/reload` while our in-memory state does not, so one left behind for a
|
|
1654
|
+
* provider that is no longer opted in would linger and could make a later recompose delete the
|
|
1655
|
+
* provider outright.
|
|
1571
1656
|
*/
|
|
1572
1657
|
releaseStale(pi, registry, config) {
|
|
1658
|
+
for (const providerId of [...this.wraps.keys()]) {
|
|
1659
|
+
if (config.providers.has(providerId)) continue;
|
|
1660
|
+
if (!isOurWrapper(registry.getRegisteredNativeProvider(providerId))) {
|
|
1661
|
+
this.wraps.delete(providerId);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
if (registry.getRegisteredProviderConfig(providerId) !== void 0) continue;
|
|
1665
|
+
this.release(pi, providerId);
|
|
1666
|
+
}
|
|
1573
1667
|
for (const [providerId, models] of this.lastRegistered) {
|
|
1574
1668
|
if (config.providers.has(providerId)) continue;
|
|
1575
1669
|
if (!isSolelyOurRegistration(registry.getRegisteredProviderConfig(providerId), models)) continue;
|
|
1576
|
-
|
|
1577
|
-
pi.unregisterProvider(providerId);
|
|
1578
|
-
this.lastRegistered.delete(providerId);
|
|
1579
|
-
} catch (error) {
|
|
1580
|
-
this.warn(`failed to release provider '${providerId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1581
|
-
}
|
|
1670
|
+
this.release(pi, providerId);
|
|
1582
1671
|
}
|
|
1583
1672
|
}
|
|
1584
1673
|
getReports() {
|
|
@@ -1591,11 +1680,190 @@ var ProviderApplier = class {
|
|
|
1591
1680
|
});
|
|
1592
1681
|
return reports.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
1593
1682
|
}
|
|
1683
|
+
track(provider, strategy, snapshot, extra) {
|
|
1684
|
+
this.tracked.set(provider.id, {
|
|
1685
|
+
provider,
|
|
1686
|
+
strategy,
|
|
1687
|
+
snapshot,
|
|
1688
|
+
refresh: extra.refresh,
|
|
1689
|
+
api: extra.api,
|
|
1690
|
+
baseUrl: extra.baseUrl
|
|
1691
|
+
});
|
|
1692
|
+
this.reports.set(provider.id, {
|
|
1693
|
+
provider: provider.id,
|
|
1694
|
+
status: "pending",
|
|
1695
|
+
strategy,
|
|
1696
|
+
reason: void 0,
|
|
1697
|
+
models: []
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
trackNative(provider, pristine) {
|
|
1701
|
+
this.track(provider, "native", [], {});
|
|
1702
|
+
if (this.wraps.get(provider.id)?.pristine === pristine) return;
|
|
1703
|
+
this.wraps.set(provider.id, new NativeWrap(pristine, {
|
|
1704
|
+
context: () => this.nativeContext(provider.id),
|
|
1705
|
+
onReports: (reports) => {
|
|
1706
|
+
this.recordNative(provider.id, reports);
|
|
1707
|
+
},
|
|
1708
|
+
warn: this.warn
|
|
1709
|
+
}));
|
|
1710
|
+
}
|
|
1711
|
+
/** `undefined` keeps a wrapper inert: it hands back the list underneath, uncompleted. */
|
|
1712
|
+
nativeContext(providerId) {
|
|
1713
|
+
const applied = this.applied;
|
|
1714
|
+
const tracked = this.tracked.get(providerId);
|
|
1715
|
+
if (applied === void 0 || tracked?.strategy !== "native") return;
|
|
1716
|
+
return {
|
|
1717
|
+
config: applied.config,
|
|
1718
|
+
provider: tracked.provider,
|
|
1719
|
+
catalog: applied.catalog,
|
|
1720
|
+
authored: applied.authored.get(providerId)
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
recordNative(providerId, models) {
|
|
1724
|
+
if (this.tracked.get(providerId)?.strategy !== "native") return;
|
|
1725
|
+
this.reports.set(providerId, {
|
|
1726
|
+
provider: providerId,
|
|
1727
|
+
status: "applied",
|
|
1728
|
+
strategy: "native",
|
|
1729
|
+
reason: void 0,
|
|
1730
|
+
models
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
applyNative(pi, providerId) {
|
|
1734
|
+
const wrap = this.wraps.get(providerId);
|
|
1735
|
+
const applied = this.applied;
|
|
1736
|
+
if (wrap === void 0 || applied === void 0) return;
|
|
1737
|
+
const fingerprint = catalogFingerprint(applied.catalog);
|
|
1738
|
+
if (this.appliedCatalog.get(providerId) === fingerprint) return;
|
|
1739
|
+
wrap.invalidate();
|
|
1740
|
+
try {
|
|
1741
|
+
pi.registerProvider(wrap.provider);
|
|
1742
|
+
this.appliedCatalog.set(providerId, fingerprint);
|
|
1743
|
+
} catch (error) {
|
|
1744
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1745
|
+
this.warn(`failed to complete provider '${providerId}': ${message}`);
|
|
1746
|
+
this.reports.set(providerId, {
|
|
1747
|
+
provider: providerId,
|
|
1748
|
+
status: "failed",
|
|
1749
|
+
strategy: "native",
|
|
1750
|
+
reason: message,
|
|
1751
|
+
models: []
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
applyRegistration(pi, providerId, tracked) {
|
|
1756
|
+
const completed = this.complete(tracked, tracked.snapshot);
|
|
1757
|
+
if (completed === void 0) return;
|
|
1758
|
+
try {
|
|
1759
|
+
const config = tracked.refresh === void 0 ? { models: completed.models } : {
|
|
1760
|
+
models: completed.models,
|
|
1761
|
+
refreshModels: tracked.refresh
|
|
1762
|
+
};
|
|
1763
|
+
pi.registerProvider(providerId, config);
|
|
1764
|
+
this.lastRegistered.set(providerId, completed.models);
|
|
1765
|
+
this.reports.set(providerId, {
|
|
1766
|
+
provider: providerId,
|
|
1767
|
+
status: "applied",
|
|
1768
|
+
strategy: tracked.strategy,
|
|
1769
|
+
reason: void 0,
|
|
1770
|
+
models: completed.reports
|
|
1771
|
+
});
|
|
1772
|
+
} catch (error) {
|
|
1773
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1774
|
+
this.warn(`failed to complete provider '${providerId}': ${message}`);
|
|
1775
|
+
this.reports.set(providerId, {
|
|
1776
|
+
provider: providerId,
|
|
1777
|
+
status: "failed",
|
|
1778
|
+
strategy: tracked.strategy,
|
|
1779
|
+
reason: message,
|
|
1780
|
+
models: completed.reports
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Wraps a sibling's own hook so the completion runs inside Pi's refresh: what the sibling returns
|
|
1786
|
+
* is the clean upstream list, and what we return is what `/model` renders once the refresh lands.
|
|
1787
|
+
*/
|
|
1788
|
+
decorate(providerId, original) {
|
|
1789
|
+
const decorator = async (context) => {
|
|
1790
|
+
const fresh = await original(context);
|
|
1791
|
+
const tracked = this.tracked.get(providerId);
|
|
1792
|
+
if (tracked === void 0 || !Array.isArray(fresh) || fresh.length === 0) return fresh;
|
|
1793
|
+
const snapshot = fresh.map((definition) => toSnapshot(definition, tracked.api, tracked.baseUrl)).filter((model) => model !== void 0);
|
|
1794
|
+
const completed = this.complete(tracked, snapshot);
|
|
1795
|
+
if (completed === void 0) return fresh;
|
|
1796
|
+
tracked.snapshot = snapshot;
|
|
1797
|
+
const byId = new Map(completed.models.map((model) => [model.id, model]));
|
|
1798
|
+
const models = fresh.map((definition) => byId.get(definition.id) ?? definition);
|
|
1799
|
+
this.lastRegistered.set(providerId, models);
|
|
1800
|
+
this.reports.set(providerId, {
|
|
1801
|
+
provider: providerId,
|
|
1802
|
+
status: "applied",
|
|
1803
|
+
strategy: "decorate",
|
|
1804
|
+
reason: void 0,
|
|
1805
|
+
models: completed.reports
|
|
1806
|
+
});
|
|
1807
|
+
return models;
|
|
1808
|
+
};
|
|
1809
|
+
const marked = decorator;
|
|
1810
|
+
marked[DECORATED] = original;
|
|
1811
|
+
return marked;
|
|
1812
|
+
}
|
|
1813
|
+
/** `undefined` until a catalog is loaded. */
|
|
1814
|
+
complete(tracked, snapshots) {
|
|
1815
|
+
const applied = this.applied;
|
|
1816
|
+
if (applied === void 0) return;
|
|
1817
|
+
const context = {
|
|
1818
|
+
config: applied.config,
|
|
1819
|
+
provider: tracked.provider,
|
|
1820
|
+
catalog: applied.catalog,
|
|
1821
|
+
authored: applied.authored.get(tracked.provider.id)
|
|
1822
|
+
};
|
|
1823
|
+
const models = [];
|
|
1824
|
+
const reports = [];
|
|
1825
|
+
for (const snapshot of snapshots) {
|
|
1826
|
+
const completion = completeModel(snapshot, context);
|
|
1827
|
+
models.push(completion.model);
|
|
1828
|
+
reports.push(completion.report);
|
|
1829
|
+
}
|
|
1830
|
+
return {
|
|
1831
|
+
models,
|
|
1832
|
+
reports
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
release(pi, providerId) {
|
|
1836
|
+
try {
|
|
1837
|
+
pi.unregisterProvider(providerId);
|
|
1838
|
+
this.lastRegistered.delete(providerId);
|
|
1839
|
+
this.wraps.delete(providerId);
|
|
1840
|
+
this.appliedCatalog.delete(providerId);
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
this.warn(`failed to release provider '${providerId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1594
1845
|
skip(providerId, reason) {
|
|
1595
1846
|
this.skipped.set(providerId, reason);
|
|
1596
1847
|
this.warn(`skipping provider '${providerId}': ${reason}`);
|
|
1597
1848
|
}
|
|
1598
1849
|
};
|
|
1850
|
+
function catalogFingerprint(catalog) {
|
|
1851
|
+
return catalog.sources.map((source) => `${source.source}@${source.fetchedAt ?? 0}#${source.entryCount}`).join("|");
|
|
1852
|
+
}
|
|
1853
|
+
function undecorate(refresh) {
|
|
1854
|
+
return refresh === void 0 ? void 0 : refresh[DECORATED] ?? refresh;
|
|
1855
|
+
}
|
|
1856
|
+
/** A registration definition may lean on the provider for `api` and `baseUrl`; a snapshot may not. */
|
|
1857
|
+
function toSnapshot(definition, api, baseUrl) {
|
|
1858
|
+
const resolvedApi = definition.api ?? api;
|
|
1859
|
+
const resolvedBaseUrl = definition.baseUrl ?? baseUrl;
|
|
1860
|
+
if (resolvedApi === void 0 || resolvedBaseUrl === void 0) return;
|
|
1861
|
+
return {
|
|
1862
|
+
...definition,
|
|
1863
|
+
api: resolvedApi,
|
|
1864
|
+
baseUrl: resolvedBaseUrl
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1599
1867
|
function sameIds(live, registered) {
|
|
1600
1868
|
if (live.length !== registered.length) return false;
|
|
1601
1869
|
const ids = new Set(registered.map((model) => model.id));
|
|
@@ -1634,13 +1902,13 @@ function createModelInfoExtension(pi, dependencies = {}) {
|
|
|
1634
1902
|
let session;
|
|
1635
1903
|
let isIdle = () => true;
|
|
1636
1904
|
function applyCatalog(snapshot) {
|
|
1637
|
-
catalog = snapshot;
|
|
1638
1905
|
if (config === void 0) return;
|
|
1639
1906
|
if (config.applyOnIdleOnly && !isIdle()) {
|
|
1640
1907
|
pending = snapshot;
|
|
1641
1908
|
return;
|
|
1642
1909
|
}
|
|
1643
1910
|
pending = void 0;
|
|
1911
|
+
catalog = snapshot;
|
|
1644
1912
|
applier.apply(pi, config, snapshot, userAuthored);
|
|
1645
1913
|
}
|
|
1646
1914
|
async function run(signal) {
|
|
@@ -1674,8 +1942,8 @@ function createModelInfoExtension(pi, dependencies = {}) {
|
|
|
1674
1942
|
if (config === void 0) return;
|
|
1675
1943
|
applier.releaseStale(pi, context.modelRegistry, config);
|
|
1676
1944
|
if (config.providers.size === 0) return;
|
|
1677
|
-
applier.capture(context.modelRegistry, config);
|
|
1678
1945
|
userAuthored = readUserAuthored(agentDir);
|
|
1946
|
+
applier.capture(context.modelRegistry, config, userAuthored);
|
|
1679
1947
|
start();
|
|
1680
1948
|
});
|
|
1681
1949
|
pi.on("before_agent_start", (_event, context) => {
|
|
@@ -1684,7 +1952,10 @@ function createModelInfoExtension(pi, dependencies = {}) {
|
|
|
1684
1952
|
if (applier.reconcile(registry)) applier.apply(pi, config, catalog, userAuthored);
|
|
1685
1953
|
});
|
|
1686
1954
|
pi.on("turn_end", () => {
|
|
1687
|
-
if (pending !== void 0 && config !== void 0)
|
|
1955
|
+
if (pending !== void 0 && config !== void 0) {
|
|
1956
|
+
catalog = pending;
|
|
1957
|
+
applier.apply(pi, config, pending, userAuthored);
|
|
1958
|
+
}
|
|
1688
1959
|
pending = void 0;
|
|
1689
1960
|
});
|
|
1690
1961
|
pi.on("session_shutdown", () => {
|