@mzwing/pi-model-info 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/LICENSE +21 -0
- package/README.md +187 -0
- package/config/config.example.json +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1715 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
- package/schemas/config.schema.json +543 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1715 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/config.ts
|
|
8
|
+
const EXTENSION_ID = "pi-model-info";
|
|
9
|
+
const COMMAND_NAME = "model-info";
|
|
10
|
+
const DEFAULT_SOURCES = ["pi.dev", "models.dev"];
|
|
11
|
+
const DEFAULT_CACHE_TTL_MS = 864e5;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
13
|
+
const DEFAULT_MAX_BYTES = 16777216;
|
|
14
|
+
const FREE_COST = {
|
|
15
|
+
input: 0,
|
|
16
|
+
output: 0,
|
|
17
|
+
cacheRead: 0,
|
|
18
|
+
cacheWrite: 0
|
|
19
|
+
};
|
|
20
|
+
/** Suffix variants every relay uses. Disable individually with `enabled: false`. */
|
|
21
|
+
const BUILTIN_RULES = [{
|
|
22
|
+
id: "free-dash",
|
|
23
|
+
kind: "suffix",
|
|
24
|
+
value: "-free",
|
|
25
|
+
override: { cost: FREE_COST }
|
|
26
|
+
}, {
|
|
27
|
+
id: "free-colon",
|
|
28
|
+
kind: "suffix",
|
|
29
|
+
value: ":free",
|
|
30
|
+
override: { cost: FREE_COST }
|
|
31
|
+
}];
|
|
32
|
+
const thinkingValue = z.union([z.string(), z.null()]).optional();
|
|
33
|
+
const thinkingLevelMapSchema = z.strictObject({
|
|
34
|
+
off: thinkingValue,
|
|
35
|
+
minimal: thinkingValue,
|
|
36
|
+
low: thinkingValue,
|
|
37
|
+
medium: thinkingValue,
|
|
38
|
+
high: thinkingValue,
|
|
39
|
+
xhigh: thinkingValue,
|
|
40
|
+
max: thinkingValue
|
|
41
|
+
});
|
|
42
|
+
const costTierSchema = z.strictObject({
|
|
43
|
+
input: z.number().min(0),
|
|
44
|
+
output: z.number().min(0),
|
|
45
|
+
cacheRead: z.number().min(0),
|
|
46
|
+
cacheWrite: z.number().min(0),
|
|
47
|
+
inputTokensAbove: z.number().int().positive()
|
|
48
|
+
});
|
|
49
|
+
const metadataOverrideSchema = z.strictObject({
|
|
50
|
+
name: z.string().trim().min(1).optional(),
|
|
51
|
+
reasoning: z.boolean().optional(),
|
|
52
|
+
input: z.array(z.enum(["text", "image"])).min(1).optional(),
|
|
53
|
+
cost: z.strictObject({
|
|
54
|
+
input: z.number().min(0).optional(),
|
|
55
|
+
output: z.number().min(0).optional(),
|
|
56
|
+
cacheRead: z.number().min(0).optional(),
|
|
57
|
+
cacheWrite: z.number().min(0).optional(),
|
|
58
|
+
tiers: z.array(costTierSchema).optional()
|
|
59
|
+
}).optional(),
|
|
60
|
+
contextWindow: z.number().int().positive().optional(),
|
|
61
|
+
maxTokens: z.number().int().positive().optional(),
|
|
62
|
+
thinkingLevelMap: thinkingLevelMapSchema.optional(),
|
|
63
|
+
compat: z.custom((value) => typeof value === "object" && value !== null && !Array.isArray(value), { message: "compat must be an object" }).optional()
|
|
64
|
+
});
|
|
65
|
+
const affixRuleSchema = z.strictObject({
|
|
66
|
+
id: z.string().trim().min(1),
|
|
67
|
+
kind: z.enum(["prefix", "suffix"]),
|
|
68
|
+
value: z.string().min(1),
|
|
69
|
+
enabled: z.boolean().optional(),
|
|
70
|
+
override: metadataOverrideSchema.optional()
|
|
71
|
+
});
|
|
72
|
+
const modelGateSchema = z.strictObject({
|
|
73
|
+
prefixes: z.array(z.string().trim().min(1)).optional(),
|
|
74
|
+
suffixes: z.array(z.string().trim().min(1)).optional(),
|
|
75
|
+
alias: z.string().trim().min(1).optional(),
|
|
76
|
+
override: metadataOverrideSchema.optional(),
|
|
77
|
+
skip: z.boolean().optional()
|
|
78
|
+
});
|
|
79
|
+
const providerOptInSchema = z.strictObject({
|
|
80
|
+
catalogProvider: z.string().trim().min(1).optional(),
|
|
81
|
+
costMultiplier: z.number().min(0).optional(),
|
|
82
|
+
costPolicy: z.enum([
|
|
83
|
+
"catalog",
|
|
84
|
+
"zero",
|
|
85
|
+
"keep"
|
|
86
|
+
]).optional(),
|
|
87
|
+
contextWindowPolicy: z.enum([
|
|
88
|
+
"catalog",
|
|
89
|
+
"min",
|
|
90
|
+
"keep"
|
|
91
|
+
]).optional(),
|
|
92
|
+
capabilityPolicy: z.enum([
|
|
93
|
+
"catalog",
|
|
94
|
+
"widen",
|
|
95
|
+
"keep"
|
|
96
|
+
]).optional(),
|
|
97
|
+
useCatalogName: z.boolean().optional(),
|
|
98
|
+
mapThinkingLevels: z.boolean().optional(),
|
|
99
|
+
allowDynamic: z.boolean().optional(),
|
|
100
|
+
models: z.record(z.string().trim().min(1), modelGateSchema).optional()
|
|
101
|
+
});
|
|
102
|
+
const configFileShape = {
|
|
103
|
+
$schema: z.string().min(1).optional(),
|
|
104
|
+
providers: z.record(z.string().trim().min(1), providerOptInSchema).optional(),
|
|
105
|
+
aliases: z.record(z.string().trim().min(1), z.string().trim().min(1)).optional(),
|
|
106
|
+
models: z.record(z.string().trim().min(1), modelGateSchema).optional(),
|
|
107
|
+
rules: z.array(affixRuleSchema).optional(),
|
|
108
|
+
builtinRules: z.boolean().optional(),
|
|
109
|
+
sources: z.array(z.enum(["pi.dev", "models.dev"])).min(1).optional(),
|
|
110
|
+
network: z.strictObject({
|
|
111
|
+
enabled: z.boolean().optional(),
|
|
112
|
+
timeoutMs: z.number().int().positive().max(12e4).optional(),
|
|
113
|
+
maxBytes: z.number().int().positive().optional()
|
|
114
|
+
}).optional(),
|
|
115
|
+
cache: z.strictObject({
|
|
116
|
+
ttlMs: z.number().int().min(0).optional(),
|
|
117
|
+
dir: z.string().trim().min(1).optional()
|
|
118
|
+
}).optional(),
|
|
119
|
+
applyOnIdleOnly: z.boolean().optional()
|
|
120
|
+
};
|
|
121
|
+
const configFileSchema = z.strictObject(configFileShape);
|
|
122
|
+
const modelInfoConfigSchema = z.strictObject({
|
|
123
|
+
...configFileShape,
|
|
124
|
+
providers: z.record(z.string().trim().min(1), providerOptInSchema).default({})
|
|
125
|
+
}).superRefine((config, context) => {
|
|
126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
127
|
+
for (const [index, rule] of (config.rules ?? []).entries()) {
|
|
128
|
+
if (seen.has(rule.id)) context.addIssue({
|
|
129
|
+
code: "custom",
|
|
130
|
+
message: `duplicate rule id '${rule.id}'`,
|
|
131
|
+
path: [
|
|
132
|
+
"rules",
|
|
133
|
+
index,
|
|
134
|
+
"id"
|
|
135
|
+
]
|
|
136
|
+
});
|
|
137
|
+
seen.add(rule.id);
|
|
138
|
+
}
|
|
139
|
+
if (config.sources && new Set(config.sources).size !== config.sources.length) context.addIssue({
|
|
140
|
+
code: "custom",
|
|
141
|
+
message: "sources must not repeat a value",
|
|
142
|
+
path: ["sources"]
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
function defaultModelInfoAgentDir() {
|
|
146
|
+
return process.env["PI_CODING_AGENT_DIR"] ?? join(homedir(), ".pi", "agent");
|
|
147
|
+
}
|
|
148
|
+
function getModelInfoConfigPaths(cwd, agentDir = defaultModelInfoAgentDir()) {
|
|
149
|
+
return {
|
|
150
|
+
globalPath: join(agentDir, "extensions", EXTENSION_ID, "config.json"),
|
|
151
|
+
projectPath: join(cwd, ".pi", "extensions", EXTENSION_ID, "config.json")
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Reads a config file, reporting a missing one as `undefined` rather than an error. */
|
|
155
|
+
function readConfigFile(path) {
|
|
156
|
+
try {
|
|
157
|
+
return readFileSync(path, "utf8");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function formatZodIssue(error) {
|
|
164
|
+
return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`).join("; ");
|
|
165
|
+
}
|
|
166
|
+
/** Returns `undefined` on failure, having recorded why; an absent file reads as an empty scope. */
|
|
167
|
+
function readScope(path, readFile, issues) {
|
|
168
|
+
let source;
|
|
169
|
+
try {
|
|
170
|
+
source = readFile(path);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
issues.push({
|
|
173
|
+
sourcePath: path,
|
|
174
|
+
message: error instanceof Error ? error.message : String(error)
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (source === void 0) return {};
|
|
179
|
+
let value;
|
|
180
|
+
try {
|
|
181
|
+
value = JSON.parse(source);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
issues.push({
|
|
184
|
+
sourcePath: path,
|
|
185
|
+
message: `invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const parsed = configFileSchema.safeParse(value);
|
|
190
|
+
if (!parsed.success) {
|
|
191
|
+
issues.push({
|
|
192
|
+
sourcePath: path,
|
|
193
|
+
message: formatZodIssue(parsed.error)
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
return parsed.data;
|
|
198
|
+
}
|
|
199
|
+
function loadModelInfoConfig(options) {
|
|
200
|
+
const { globalPath, projectPath } = getModelInfoConfigPaths(options.cwd, options.agentDir);
|
|
201
|
+
const readFile = options.readFile ?? readConfigFile;
|
|
202
|
+
const issues = [];
|
|
203
|
+
const globalConfig = readScope(globalPath, readFile, issues);
|
|
204
|
+
const projectConfig = readScope(projectPath, readFile, issues);
|
|
205
|
+
if (globalConfig === void 0 || projectConfig === void 0) return {
|
|
206
|
+
config: void 0,
|
|
207
|
+
issues,
|
|
208
|
+
globalPath,
|
|
209
|
+
projectPath
|
|
210
|
+
};
|
|
211
|
+
const merged = modelInfoConfigSchema.safeParse({
|
|
212
|
+
...globalConfig,
|
|
213
|
+
...projectConfig
|
|
214
|
+
});
|
|
215
|
+
if (!merged.success) {
|
|
216
|
+
issues.push({
|
|
217
|
+
sourcePath: projectPath,
|
|
218
|
+
message: formatZodIssue(merged.error)
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
config: void 0,
|
|
222
|
+
issues,
|
|
223
|
+
globalPath,
|
|
224
|
+
projectPath
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
config: merged.data,
|
|
229
|
+
issues,
|
|
230
|
+
globalPath,
|
|
231
|
+
projectPath
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Model ids routinely contain `/`, so a flat `"provider/model"` key is only unambiguous because
|
|
236
|
+
* provider ids are known: split at the FIRST separator and require the head to be an opted-in one.
|
|
237
|
+
*/
|
|
238
|
+
function splitFlatKey(key, providers) {
|
|
239
|
+
const separator = key.indexOf("/");
|
|
240
|
+
if (separator <= 0 || separator === key.length - 1) return;
|
|
241
|
+
const provider = providers.get(key.slice(0, separator));
|
|
242
|
+
return provider === void 0 ? void 0 : [provider, key.slice(separator + 1)];
|
|
243
|
+
}
|
|
244
|
+
function orderRules(rules) {
|
|
245
|
+
return rules.map((rule, ordinal) => ({
|
|
246
|
+
rule,
|
|
247
|
+
ordinal
|
|
248
|
+
})).sort((a, b) => b.rule.value.length - a.rule.value.length || a.ordinal - b.ordinal).map((entry) => entry.rule);
|
|
249
|
+
}
|
|
250
|
+
/** The flat `models` and `aliases` sugar, as `[section, key, gate]` in the order they are folded in. */
|
|
251
|
+
function flatGates(config) {
|
|
252
|
+
return [...Object.entries(config.models ?? {}).map(([key, gate]) => [
|
|
253
|
+
"models",
|
|
254
|
+
key,
|
|
255
|
+
gate
|
|
256
|
+
]), ...Object.entries(config.aliases ?? {}).map(([key, alias]) => [
|
|
257
|
+
"aliases",
|
|
258
|
+
key,
|
|
259
|
+
{ alias }
|
|
260
|
+
])];
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Materialises defaults, folds the flat sugar into per-provider gates, and orders the affix rules.
|
|
264
|
+
* Problems here are reported, not fatal: an unusable entry is dropped and the rest still applies.
|
|
265
|
+
*/
|
|
266
|
+
function resolveModelInfoConfig(config, sourcePath) {
|
|
267
|
+
const issues = [];
|
|
268
|
+
const providers = /* @__PURE__ */ new Map();
|
|
269
|
+
for (const [id, optIn] of Object.entries(config.providers)) providers.set(id, {
|
|
270
|
+
id,
|
|
271
|
+
catalogProvider: optIn.catalogProvider,
|
|
272
|
+
costMultiplier: optIn.costMultiplier ?? 1,
|
|
273
|
+
costPolicy: optIn.costPolicy ?? "catalog",
|
|
274
|
+
contextWindowPolicy: optIn.contextWindowPolicy ?? "catalog",
|
|
275
|
+
capabilityPolicy: optIn.capabilityPolicy ?? "catalog",
|
|
276
|
+
useCatalogName: optIn.useCatalogName ?? false,
|
|
277
|
+
mapThinkingLevels: optIn.mapThinkingLevels ?? false,
|
|
278
|
+
allowDynamic: optIn.allowDynamic ?? false,
|
|
279
|
+
models: new Map(Object.entries(optIn.models ?? {}))
|
|
280
|
+
});
|
|
281
|
+
for (const [section, key, gate] of flatGates(config)) {
|
|
282
|
+
const split = splitFlatKey(key, providers);
|
|
283
|
+
if (split === void 0) {
|
|
284
|
+
issues.push({
|
|
285
|
+
sourcePath,
|
|
286
|
+
message: `${section}['${key}'] does not start with an opted-in provider id`
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const [provider, modelId] = split;
|
|
291
|
+
provider.models.set(modelId, {
|
|
292
|
+
...provider.models.get(modelId),
|
|
293
|
+
...gate
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const declared = [...config.rules ?? [], ...config.builtinRules === false ? [] : BUILTIN_RULES];
|
|
297
|
+
const enabled = declared.filter((rule) => rule.enabled !== false);
|
|
298
|
+
const ruleIds = new Set(declared.map((rule) => rule.id));
|
|
299
|
+
for (const provider of providers.values()) for (const [modelId, gate] of provider.models) {
|
|
300
|
+
const known = (id) => {
|
|
301
|
+
if (ruleIds.has(id)) return true;
|
|
302
|
+
issues.push({
|
|
303
|
+
sourcePath,
|
|
304
|
+
message: `providers['${provider.id}'].models['${modelId}'] references unknown rule '${id}'`
|
|
305
|
+
});
|
|
306
|
+
return false;
|
|
307
|
+
};
|
|
308
|
+
const prefixes = gate.prefixes?.filter(known);
|
|
309
|
+
const suffixes = gate.suffixes?.filter(known);
|
|
310
|
+
provider.models.set(modelId, {
|
|
311
|
+
...gate,
|
|
312
|
+
...prefixes === void 0 ? {} : { prefixes },
|
|
313
|
+
...suffixes === void 0 ? {} : { suffixes }
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
config: {
|
|
318
|
+
providers,
|
|
319
|
+
prefixRules: orderRules(enabled.filter((rule) => rule.kind === "prefix")),
|
|
320
|
+
suffixRules: orderRules(enabled.filter((rule) => rule.kind === "suffix")),
|
|
321
|
+
sources: config.sources ?? DEFAULT_SOURCES,
|
|
322
|
+
network: {
|
|
323
|
+
enabled: config.network?.enabled ?? true,
|
|
324
|
+
timeoutMs: config.network?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
325
|
+
maxBytes: config.network?.maxBytes ?? DEFAULT_MAX_BYTES
|
|
326
|
+
},
|
|
327
|
+
cache: {
|
|
328
|
+
ttlMs: config.cache?.ttlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
329
|
+
dir: config.cache?.dir
|
|
330
|
+
},
|
|
331
|
+
applyOnIdleOnly: config.applyOnIdleOnly ?? true
|
|
332
|
+
},
|
|
333
|
+
issues
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/cache.ts
|
|
339
|
+
const CACHE_VERSION = 1;
|
|
340
|
+
const FILE_NAMES = {
|
|
341
|
+
"pi.dev": "pi-dev.json",
|
|
342
|
+
"models.dev": "models-dev.json"
|
|
343
|
+
};
|
|
344
|
+
const defaultFileSystem = {
|
|
345
|
+
readFile(path) {
|
|
346
|
+
try {
|
|
347
|
+
return readFileSync(path, "utf8");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
writeFile(path, data) {
|
|
354
|
+
writeFileSync(path, data, "utf8");
|
|
355
|
+
},
|
|
356
|
+
rename(from, to) {
|
|
357
|
+
renameSync(from, to);
|
|
358
|
+
},
|
|
359
|
+
mkdir(path) {
|
|
360
|
+
mkdirSync(path, { recursive: true });
|
|
361
|
+
},
|
|
362
|
+
unlink(path) {
|
|
363
|
+
unlinkSync(path);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function toNormalizedSource(envelope) {
|
|
367
|
+
return {
|
|
368
|
+
source: envelope.source,
|
|
369
|
+
entries: envelope.entries,
|
|
370
|
+
vendors: new Map(envelope.vendors)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
var CatalogCache = class {
|
|
374
|
+
dir;
|
|
375
|
+
fileSystem;
|
|
376
|
+
constructor(options = {}) {
|
|
377
|
+
const agentDir = options.agentDir ?? defaultModelInfoAgentDir();
|
|
378
|
+
this.dir = options.dir ?? join(agentDir, "extensions", "pi-model-info", "cache");
|
|
379
|
+
this.fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* A corrupt or half-written envelope reads as absent and is left on disk: the next successful
|
|
383
|
+
* fetch replaces it, and until then a copy another machine can still read is not worth destroying.
|
|
384
|
+
*/
|
|
385
|
+
read(source) {
|
|
386
|
+
let raw;
|
|
387
|
+
try {
|
|
388
|
+
raw = this.fileSystem.readFile(this.path(source));
|
|
389
|
+
} catch {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (raw === void 0) return;
|
|
393
|
+
let parsed;
|
|
394
|
+
try {
|
|
395
|
+
parsed = JSON.parse(raw);
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
400
|
+
const envelope = parsed;
|
|
401
|
+
return envelope.version === 1 && envelope.source === source && Array.isArray(envelope.entries) && Array.isArray(envelope.vendors) && typeof envelope.fetchedAt === "number" && envelope.entries.length === envelope.entryCount ? envelope : void 0;
|
|
402
|
+
}
|
|
403
|
+
write(envelope) {
|
|
404
|
+
const path = this.path(envelope.source);
|
|
405
|
+
const temporary = `${path}.tmp`;
|
|
406
|
+
try {
|
|
407
|
+
this.fileSystem.mkdir(dirname(path));
|
|
408
|
+
this.fileSystem.writeFile(temporary, `${JSON.stringify(envelope)}\n`);
|
|
409
|
+
this.fileSystem.rename(temporary, path);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
try {
|
|
412
|
+
this.fileSystem.unlink(temporary);
|
|
413
|
+
} catch {}
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
path(source) {
|
|
418
|
+
return join(this.dir, FILE_NAMES[source]);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/compact.ts
|
|
424
|
+
/** Drops undefined values, so `exactOptionalPropertyTypes` sees omission rather than an undefined slot. */
|
|
425
|
+
function compact(value) {
|
|
426
|
+
return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== void 0));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/types.ts
|
|
431
|
+
const THINKING_LEVELS = [
|
|
432
|
+
"off",
|
|
433
|
+
"minimal",
|
|
434
|
+
"low",
|
|
435
|
+
"medium",
|
|
436
|
+
"high",
|
|
437
|
+
"xhigh",
|
|
438
|
+
"max"
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/catalog-sources.ts
|
|
443
|
+
const PI_DEV_URL = "https://pi.dev/api/models";
|
|
444
|
+
const MODELS_DEV_URL = "https://models.dev/models.json";
|
|
445
|
+
function isRecord$1(value) {
|
|
446
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
447
|
+
}
|
|
448
|
+
/** Remote JSON delivers `__proto__` and friends as ordinary own keys; dropping them keeps them out of index keys. */
|
|
449
|
+
const FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
|
|
450
|
+
"__proto__",
|
|
451
|
+
"constructor",
|
|
452
|
+
"prototype"
|
|
453
|
+
]);
|
|
454
|
+
function entriesOf(value) {
|
|
455
|
+
if (!isRecord$1(value)) return [];
|
|
456
|
+
return Object.entries(value).filter(([key]) => !FORBIDDEN_KEYS.has(key));
|
|
457
|
+
}
|
|
458
|
+
function positiveInt(value) {
|
|
459
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
460
|
+
}
|
|
461
|
+
function nonNegative(value) {
|
|
462
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
463
|
+
}
|
|
464
|
+
function text(value) {
|
|
465
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
466
|
+
}
|
|
467
|
+
function flag(value) {
|
|
468
|
+
return typeof value === "boolean" ? value : void 0;
|
|
469
|
+
}
|
|
470
|
+
function bareId(id) {
|
|
471
|
+
const separator = id.indexOf("/");
|
|
472
|
+
return separator > 0 && separator < id.length - 1 ? id.slice(separator + 1) : id;
|
|
473
|
+
}
|
|
474
|
+
function vendorOf(id) {
|
|
475
|
+
const separator = id.indexOf("/");
|
|
476
|
+
return separator > 0 && separator < id.length - 1 ? id.slice(0, separator) : void 0;
|
|
477
|
+
}
|
|
478
|
+
/** Pi accepts only `text` and `image`; absent modalities mean "unknown", not "text only". */
|
|
479
|
+
function toModelInput(modalities) {
|
|
480
|
+
if (!isRecord$1(modalities) || !Array.isArray(modalities["input"])) return;
|
|
481
|
+
const declared = new Set(modalities["input"].filter((value) => typeof value === "string"));
|
|
482
|
+
const input = ["text"];
|
|
483
|
+
if (declared.has("image")) input.push("image");
|
|
484
|
+
return input;
|
|
485
|
+
}
|
|
486
|
+
function toCost(raw) {
|
|
487
|
+
if (!isRecord$1(raw)) return;
|
|
488
|
+
const input = nonNegative(raw["input"]);
|
|
489
|
+
const output = nonNegative(raw["output"]);
|
|
490
|
+
if (input === void 0 || output === void 0) return;
|
|
491
|
+
return compact({
|
|
492
|
+
input,
|
|
493
|
+
output,
|
|
494
|
+
cacheRead: nonNegative(raw["cacheRead"] ?? raw["cache_read"]) ?? 0,
|
|
495
|
+
cacheWrite: nonNegative(raw["cacheWrite"] ?? raw["cache_write"]) ?? 0,
|
|
496
|
+
tiers: toTiers(raw)
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function toTier(raw, fallbackThreshold) {
|
|
500
|
+
if (!isRecord$1(raw)) return;
|
|
501
|
+
const input = nonNegative(raw["input"]);
|
|
502
|
+
const output = nonNegative(raw["output"]);
|
|
503
|
+
const above = positiveInt(raw["inputTokensAbove"]) ?? (isRecord$1(raw["tier"]) ? positiveInt(raw["tier"]["size"]) : void 0) ?? fallbackThreshold;
|
|
504
|
+
if (input === void 0 || output === void 0 || above === void 0) return;
|
|
505
|
+
return {
|
|
506
|
+
input,
|
|
507
|
+
output,
|
|
508
|
+
cacheRead: nonNegative(raw["cacheRead"] ?? raw["cache_read"]) ?? 0,
|
|
509
|
+
cacheWrite: nonNegative(raw["cacheWrite"] ?? raw["cache_write"]) ?? 0,
|
|
510
|
+
inputTokensAbove: above
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function toTiers(raw) {
|
|
514
|
+
if (Array.isArray(raw["tiers"])) {
|
|
515
|
+
const tiers = [];
|
|
516
|
+
for (const entry of raw["tiers"]) {
|
|
517
|
+
const tier = toTier(entry);
|
|
518
|
+
if (tier !== void 0) tiers.push(tier);
|
|
519
|
+
}
|
|
520
|
+
return tiers.length > 0 ? tiers : void 0;
|
|
521
|
+
}
|
|
522
|
+
const legacy = toTier(raw["context_over_200k"], 2e5);
|
|
523
|
+
return legacy === void 0 ? void 0 : [legacy];
|
|
524
|
+
}
|
|
525
|
+
function toThinkingLevelMap(raw) {
|
|
526
|
+
if (!isRecord$1(raw)) return;
|
|
527
|
+
const map = {};
|
|
528
|
+
let mapped = false;
|
|
529
|
+
for (const level of THINKING_LEVELS) {
|
|
530
|
+
const value = raw[level];
|
|
531
|
+
if (typeof value === "string" || value === null) {
|
|
532
|
+
map[level] = value;
|
|
533
|
+
mapped = true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return mapped ? map : void 0;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* models.dev describes reasoning as options rather than a level map. Only the `effort` form maps to
|
|
540
|
+
* the strings Pi sends; `toggle` and `budget_tokens` carry no level names, and inventing one is a
|
|
541
|
+
* 400 on every turn.
|
|
542
|
+
*/
|
|
543
|
+
function reasoningOptionsToThinkingLevelMap(raw) {
|
|
544
|
+
if (!Array.isArray(raw)) return;
|
|
545
|
+
const effort = raw.find((option) => isRecord$1(option) && option["type"] === "effort" && Array.isArray(option["values"]));
|
|
546
|
+
if (effort === void 0) return;
|
|
547
|
+
const values = new Set(effort["values"].filter((value) => typeof value === "string"));
|
|
548
|
+
const map = {};
|
|
549
|
+
let mapped = false;
|
|
550
|
+
for (const level of THINKING_LEVELS) {
|
|
551
|
+
const name = level === "off" ? "none" : level;
|
|
552
|
+
if (values.has(name)) {
|
|
553
|
+
map[level] = name;
|
|
554
|
+
mapped = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return mapped ? map : void 0;
|
|
558
|
+
}
|
|
559
|
+
/** `{ providerId: { modelId: Model } }`, already in Pi's own shape. */
|
|
560
|
+
function normalizePiDev(payload) {
|
|
561
|
+
const entries = [];
|
|
562
|
+
for (const [providerId, models] of entriesOf(payload)) for (const [modelId, raw] of entriesOf(models)) {
|
|
563
|
+
if (!isRecord$1(raw)) continue;
|
|
564
|
+
const id = text(raw["id"]) ?? modelId;
|
|
565
|
+
const input = Array.isArray(raw["input"]) ? raw["input"].filter((value) => value === "text" || value === "image") : void 0;
|
|
566
|
+
entries.push(compact({
|
|
567
|
+
source: "pi.dev",
|
|
568
|
+
sourceProvider: providerId,
|
|
569
|
+
sourceId: id,
|
|
570
|
+
canonicalId: `${providerId}/${id}`,
|
|
571
|
+
api: text(raw["api"]),
|
|
572
|
+
metadata: compact({
|
|
573
|
+
name: text(raw["name"]),
|
|
574
|
+
reasoning: flag(raw["reasoning"]),
|
|
575
|
+
input: input === void 0 || input.length === 0 ? void 0 : input,
|
|
576
|
+
cost: toCost(raw["cost"]),
|
|
577
|
+
contextWindow: positiveInt(raw["contextWindow"]),
|
|
578
|
+
maxTokens: positiveInt(raw["maxTokens"]),
|
|
579
|
+
thinkingLevelMap: toThinkingLevelMap(raw["thinkingLevelMap"]),
|
|
580
|
+
compat: isRecord$1(raw["compat"]) ? raw["compat"] : void 0
|
|
581
|
+
})
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
source: "pi.dev",
|
|
586
|
+
entries,
|
|
587
|
+
vendors: /* @__PURE__ */ new Map()
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/** `{ "vendor/model": metadata }` — provider-agnostic, and carries no pricing. */
|
|
591
|
+
function normalizeModelsDev(payload) {
|
|
592
|
+
const entries = [];
|
|
593
|
+
const vendors = /* @__PURE__ */ new Map();
|
|
594
|
+
for (const [key, raw] of entriesOf(payload)) {
|
|
595
|
+
if (!isRecord$1(raw)) continue;
|
|
596
|
+
const canonicalId = text(raw["id"]) ?? key;
|
|
597
|
+
const vendor = vendorOf(canonicalId);
|
|
598
|
+
const bare = bareId(canonicalId);
|
|
599
|
+
if (vendor !== void 0) vendors.set(bare.toLowerCase(), vendor);
|
|
600
|
+
const limit = isRecord$1(raw["limit"]) ? raw["limit"] : void 0;
|
|
601
|
+
entries.push({
|
|
602
|
+
source: "models.dev",
|
|
603
|
+
sourceProvider: vendor,
|
|
604
|
+
sourceId: bare,
|
|
605
|
+
canonicalId,
|
|
606
|
+
metadata: compact({
|
|
607
|
+
name: text(raw["name"]),
|
|
608
|
+
reasoning: flag(raw["reasoning"]),
|
|
609
|
+
input: toModelInput(raw["modalities"]),
|
|
610
|
+
contextWindow: limit === void 0 ? void 0 : positiveInt(limit["context"]),
|
|
611
|
+
maxTokens: limit === void 0 ? void 0 : positiveInt(limit["output"]),
|
|
612
|
+
thinkingLevelMap: reasoningOptionsToThinkingLevelMap(raw["reasoning_options"])
|
|
613
|
+
})
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
source: "models.dev",
|
|
618
|
+
entries,
|
|
619
|
+
vendors
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/catalog-index.ts
|
|
625
|
+
/** NUL cannot appear in a provider or model id, so the composite key is unambiguous. */
|
|
626
|
+
const SCOPE_SEPARATOR = String.fromCharCode(0);
|
|
627
|
+
function scopedKey(provider, id) {
|
|
628
|
+
return `${provider.toLowerCase()}${SCOPE_SEPARATOR}${id.toLowerCase()}`;
|
|
629
|
+
}
|
|
630
|
+
function push(map, key, entry) {
|
|
631
|
+
const bucket = map.get(key);
|
|
632
|
+
if (bucket === void 0) map.set(key, [entry]);
|
|
633
|
+
else bucket.push(entry);
|
|
634
|
+
}
|
|
635
|
+
/** Inserts in source-priority order, so every bucket is ranked and the resolver never sorts by source. */
|
|
636
|
+
function buildCatalogIndex(sources, order) {
|
|
637
|
+
const rank = new Map(order.map((source, position) => [source, position]));
|
|
638
|
+
const ordered = sources.filter((source) => rank.has(source.source)).sort((a, b) => (rank.get(a.source) ?? 0) - (rank.get(b.source) ?? 0));
|
|
639
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
640
|
+
const exact = /* @__PURE__ */ new Map();
|
|
641
|
+
const bare = /* @__PURE__ */ new Map();
|
|
642
|
+
const vendors = /* @__PURE__ */ new Map();
|
|
643
|
+
for (const source of ordered) {
|
|
644
|
+
for (const [key, vendor] of source.vendors) if (!vendors.has(key)) vendors.set(key, vendor);
|
|
645
|
+
for (const entry of source.entries) {
|
|
646
|
+
const short = bareId(entry.sourceId);
|
|
647
|
+
if (entry.sourceProvider !== void 0) {
|
|
648
|
+
push(scoped, scopedKey(entry.sourceProvider, entry.sourceId), entry);
|
|
649
|
+
if (short !== entry.sourceId) push(scoped, scopedKey(entry.sourceProvider, short), entry);
|
|
650
|
+
}
|
|
651
|
+
push(exact, entry.sourceId.toLowerCase(), entry);
|
|
652
|
+
if (entry.canonicalId !== entry.sourceId) push(exact, entry.canonicalId.toLowerCase(), entry);
|
|
653
|
+
push(bare, short.toLowerCase(), entry);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
scoped,
|
|
658
|
+
exact,
|
|
659
|
+
bare,
|
|
660
|
+
vendors
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/fetcher.ts
|
|
666
|
+
function describe(error) {
|
|
667
|
+
return error instanceof Error ? error.message : String(error);
|
|
668
|
+
}
|
|
669
|
+
/** Reads through the stream so an oversized payload is abandoned rather than buffered whole. */
|
|
670
|
+
async function readCapped(response, maxBytes) {
|
|
671
|
+
const body = response.body;
|
|
672
|
+
if (body === null) return "";
|
|
673
|
+
const reader = body.getReader();
|
|
674
|
+
const decoder = new TextDecoder();
|
|
675
|
+
const chunks = [];
|
|
676
|
+
let size = 0;
|
|
677
|
+
try {
|
|
678
|
+
for (;;) {
|
|
679
|
+
const chunk = await reader.read();
|
|
680
|
+
if (chunk.done) break;
|
|
681
|
+
size += chunk.value.byteLength;
|
|
682
|
+
if (size > maxBytes) throw new Error(`response exceeded ${maxBytes} bytes`);
|
|
683
|
+
chunks.push(decoder.decode(chunk.value, { stream: true }));
|
|
684
|
+
}
|
|
685
|
+
} finally {
|
|
686
|
+
reader.releaseLock();
|
|
687
|
+
}
|
|
688
|
+
chunks.push(decoder.decode());
|
|
689
|
+
return chunks.join("");
|
|
690
|
+
}
|
|
691
|
+
const catalogFetcher = { async get(request, signal) {
|
|
692
|
+
let url;
|
|
693
|
+
try {
|
|
694
|
+
url = new URL(request.url);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
status: "error",
|
|
698
|
+
message: `invalid url: ${describe(error)}`
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (url.protocol !== "https:") return {
|
|
702
|
+
status: "error",
|
|
703
|
+
message: `refusing non-https catalog url '${request.url}'`
|
|
704
|
+
};
|
|
705
|
+
const headers = new Headers({ accept: "application/json" });
|
|
706
|
+
if (request.etag !== void 0) headers.set("if-none-match", request.etag);
|
|
707
|
+
if (request.lastModified !== void 0) headers.set("if-modified-since", request.lastModified);
|
|
708
|
+
try {
|
|
709
|
+
const response = await fetch(url, {
|
|
710
|
+
headers,
|
|
711
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(request.timeoutMs)])
|
|
712
|
+
});
|
|
713
|
+
if (response.status === 304) {
|
|
714
|
+
await response.body?.cancel();
|
|
715
|
+
return { status: "not-modified" };
|
|
716
|
+
}
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
await response.body?.cancel();
|
|
719
|
+
return {
|
|
720
|
+
status: "error",
|
|
721
|
+
message: `HTTP ${response.status}`
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
const text = await readCapped(response, request.maxBytes);
|
|
725
|
+
if (text.length === 0) return {
|
|
726
|
+
status: "error",
|
|
727
|
+
message: "empty response"
|
|
728
|
+
};
|
|
729
|
+
return {
|
|
730
|
+
status: "ok",
|
|
731
|
+
body: JSON.parse(text),
|
|
732
|
+
etag: response.headers.get("etag") ?? void 0,
|
|
733
|
+
lastModified: response.headers.get("last-modified") ?? void 0
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return {
|
|
737
|
+
status: "error",
|
|
738
|
+
message: describe(error)
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
} };
|
|
742
|
+
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/catalog.ts
|
|
745
|
+
const SOURCES = {
|
|
746
|
+
"pi.dev": {
|
|
747
|
+
url: PI_DEV_URL,
|
|
748
|
+
normalize: normalizePiDev
|
|
749
|
+
},
|
|
750
|
+
"models.dev": {
|
|
751
|
+
url: MODELS_DEV_URL,
|
|
752
|
+
normalize: normalizeModelsDev
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var CatalogStore = class {
|
|
756
|
+
cache;
|
|
757
|
+
fetcher;
|
|
758
|
+
now;
|
|
759
|
+
random;
|
|
760
|
+
errors = /* @__PURE__ */ new Map();
|
|
761
|
+
constructor(deps = {}) {
|
|
762
|
+
this.cache = deps.cache ?? new CatalogCache();
|
|
763
|
+
this.fetcher = deps.fetcher ?? catalogFetcher;
|
|
764
|
+
this.now = deps.now ?? (() => Date.now());
|
|
765
|
+
this.random = deps.random ?? Math.random;
|
|
766
|
+
}
|
|
767
|
+
/** Cache only. Safe to call before any network work has happened. */
|
|
768
|
+
load(config) {
|
|
769
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
770
|
+
for (const source of config.sources) {
|
|
771
|
+
const cached = this.cache.read(source);
|
|
772
|
+
if (cached !== void 0) loaded.set(source, cached);
|
|
773
|
+
}
|
|
774
|
+
return this.snapshot(config, loaded);
|
|
775
|
+
}
|
|
776
|
+
async refresh(config, signal, force = false) {
|
|
777
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const source of config.sources) {
|
|
779
|
+
const envelope = await this.refreshOne(config, source, signal, force);
|
|
780
|
+
if (envelope !== void 0) loaded.set(source, envelope);
|
|
781
|
+
if (signal.aborted) break;
|
|
782
|
+
}
|
|
783
|
+
return this.snapshot(config, loaded);
|
|
784
|
+
}
|
|
785
|
+
async refreshOne(config, source, signal, force) {
|
|
786
|
+
const descriptor = SOURCES[source];
|
|
787
|
+
const cached = this.cache.read(source);
|
|
788
|
+
const ttl = config.cache.ttlMs * (.9 + this.random() * .2);
|
|
789
|
+
if (!force && cached !== void 0 && this.now() - cached.fetchedAt < ttl) {
|
|
790
|
+
this.errors.delete(source);
|
|
791
|
+
return cached;
|
|
792
|
+
}
|
|
793
|
+
if (!config.network.enabled || signal.aborted) return cached;
|
|
794
|
+
const outcome = await this.fetcher.get({
|
|
795
|
+
url: descriptor.url,
|
|
796
|
+
etag: cached?.etag,
|
|
797
|
+
lastModified: cached?.lastModified,
|
|
798
|
+
timeoutMs: config.network.timeoutMs,
|
|
799
|
+
maxBytes: config.network.maxBytes
|
|
800
|
+
}, signal);
|
|
801
|
+
if (outcome.status === "not-modified") {
|
|
802
|
+
if (cached === void 0) {
|
|
803
|
+
this.errors.set(source, "304 with no cached copy");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const renewed = {
|
|
807
|
+
...cached,
|
|
808
|
+
fetchedAt: this.now()
|
|
809
|
+
};
|
|
810
|
+
this.persist(source, renewed);
|
|
811
|
+
this.errors.delete(source);
|
|
812
|
+
return renewed;
|
|
813
|
+
}
|
|
814
|
+
if (outcome.status === "error") {
|
|
815
|
+
this.errors.set(source, outcome.message);
|
|
816
|
+
return cached;
|
|
817
|
+
}
|
|
818
|
+
const normalized = descriptor.normalize(outcome.body);
|
|
819
|
+
if (normalized.entries.length === 0) {
|
|
820
|
+
this.errors.set(source, "catalog contained no usable models");
|
|
821
|
+
return cached;
|
|
822
|
+
}
|
|
823
|
+
const envelope = {
|
|
824
|
+
version: 1,
|
|
825
|
+
source,
|
|
826
|
+
etag: outcome.etag,
|
|
827
|
+
lastModified: outcome.lastModified,
|
|
828
|
+
fetchedAt: this.now(),
|
|
829
|
+
entryCount: normalized.entries.length,
|
|
830
|
+
entries: normalized.entries,
|
|
831
|
+
vendors: [...normalized.vendors]
|
|
832
|
+
};
|
|
833
|
+
this.persist(source, envelope);
|
|
834
|
+
this.errors.delete(source);
|
|
835
|
+
return envelope;
|
|
836
|
+
}
|
|
837
|
+
persist(source, envelope) {
|
|
838
|
+
try {
|
|
839
|
+
this.cache.write(envelope);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
this.errors.set(source, `cache write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
snapshot(config, loaded) {
|
|
845
|
+
const sources = config.sources.map((source) => {
|
|
846
|
+
const envelope = loaded.get(source);
|
|
847
|
+
return {
|
|
848
|
+
source,
|
|
849
|
+
fetchedAt: envelope?.fetchedAt,
|
|
850
|
+
entryCount: envelope?.entries.length ?? 0,
|
|
851
|
+
lastError: this.errors.get(source)
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
const normalized = config.sources.map((source) => loaded.get(source)).filter((envelope) => envelope !== void 0).map(toNormalizedSource);
|
|
855
|
+
return {
|
|
856
|
+
index: buildCatalogIndex(normalized, config.sources),
|
|
857
|
+
status: normalized.some((source) => source.entries.length > 0) ? "ready" : "unavailable",
|
|
858
|
+
sources
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
//#endregion
|
|
864
|
+
//#region src/command.ts
|
|
865
|
+
const COMPLETION_LIMIT = 50;
|
|
866
|
+
function splitReference(reference) {
|
|
867
|
+
const separator = reference.indexOf("/");
|
|
868
|
+
return separator > 0 ? {
|
|
869
|
+
providerId: reference.slice(0, separator),
|
|
870
|
+
modelId: reference.slice(separator + 1)
|
|
871
|
+
} : {
|
|
872
|
+
providerId: void 0,
|
|
873
|
+
modelId: reference
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function age(now, fetchedAt) {
|
|
877
|
+
if (fetchedAt === void 0) return "never fetched";
|
|
878
|
+
const minutes = Math.max(0, Math.round((now - fetchedAt) / 6e4));
|
|
879
|
+
return minutes < 60 ? `${minutes}m ago` : `${Math.round(minutes / 60)}h ago`;
|
|
880
|
+
}
|
|
881
|
+
function countByKind(models) {
|
|
882
|
+
const counts = {
|
|
883
|
+
resolved: 0,
|
|
884
|
+
ambiguous: 0,
|
|
885
|
+
unresolved: 0
|
|
886
|
+
};
|
|
887
|
+
for (const model of models) counts[model.resolution.kind] += 1;
|
|
888
|
+
return counts;
|
|
889
|
+
}
|
|
890
|
+
function formatSummary(reports, catalog, issues, now) {
|
|
891
|
+
const lines = [];
|
|
892
|
+
if (reports.length === 0) lines.push("No providers opted in. Add one under \"providers\" in the config to complete its models.");
|
|
893
|
+
for (const report of reports) {
|
|
894
|
+
if (report.status === "skipped" || report.status === "failed") {
|
|
895
|
+
lines.push(`${report.provider}: ${report.status} — ${report.reason ?? "no reason given"}`);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
const counts = countByKind(report.models);
|
|
899
|
+
lines.push(`${report.provider}: ${counts.resolved} completed, ${counts.ambiguous} ambiguous, ${counts.unresolved} unresolved (${report.models.length} models)`);
|
|
900
|
+
}
|
|
901
|
+
if (catalog === void 0) lines.push("", "catalogs: not loaded yet");
|
|
902
|
+
else {
|
|
903
|
+
lines.push("", catalog.status === "ready" ? "catalogs:" : "catalogs: unavailable — nothing was applied");
|
|
904
|
+
for (const source of catalog.sources) {
|
|
905
|
+
const error = source.lastError === void 0 ? "" : ` — ${source.lastError}`;
|
|
906
|
+
lines.push(` ${source.source}: ${source.entryCount} entries, ${age(now, source.fetchedAt)}${error}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (issues.length > 0) {
|
|
910
|
+
lines.push("", "config issues:");
|
|
911
|
+
for (const issue of issues) lines.push(` ${issue.sourcePath}: ${issue.message}`);
|
|
912
|
+
}
|
|
913
|
+
return lines.join("\n");
|
|
914
|
+
}
|
|
915
|
+
function formatDetail(reports, reference, effective) {
|
|
916
|
+
const { providerId, modelId } = splitReference(reference);
|
|
917
|
+
const matches = reports.flatMap((report) => report.models.filter((model) => model.id === modelId && (providerId === void 0 || report.provider === providerId)).map((model) => ({
|
|
918
|
+
report,
|
|
919
|
+
model
|
|
920
|
+
})));
|
|
921
|
+
const first = matches[0];
|
|
922
|
+
if (first === void 0) return `No completed model matches '${reference}'. Run /${COMMAND_NAME} to see what is covered.`;
|
|
923
|
+
const { report, model } = first;
|
|
924
|
+
const lines = [`requested: ${report.provider}/${model.id}`];
|
|
925
|
+
if (model.resolution.kind === "resolved") {
|
|
926
|
+
const { entry, matchKind, prefixRule, suffixRule } = model.resolution;
|
|
927
|
+
lines.push(`canonical: ${entry.canonicalId} (${entry.source})`);
|
|
928
|
+
lines.push(`match: ${matchKind}`);
|
|
929
|
+
const rules = [prefixRule?.id, suffixRule?.id].filter((id) => id !== void 0);
|
|
930
|
+
if (rules.length > 0) lines.push(`rule: ${rules.join(", ")}`);
|
|
931
|
+
} else if (model.resolution.kind === "ambiguous") {
|
|
932
|
+
lines.push("match: ambiguous — nothing was applied");
|
|
933
|
+
lines.push("candidates:");
|
|
934
|
+
for (const candidate of model.resolution.candidates) lines.push(` ${candidate.canonicalId} (${candidate.source})`);
|
|
935
|
+
lines.push("Add an alias for this model to choose one.");
|
|
936
|
+
} else lines.push(`match: unresolved (${model.resolution.reason})`);
|
|
937
|
+
const source = (field) => {
|
|
938
|
+
const origin = model.provenance.get(field);
|
|
939
|
+
return origin === void 0 || origin === "existing" ? "" : ` from ${origin}`;
|
|
940
|
+
};
|
|
941
|
+
lines.push("");
|
|
942
|
+
lines.push(`context: ${model.model.contextWindow}${source("contextWindow")}`);
|
|
943
|
+
lines.push(`maxTokens: ${model.model.maxTokens}${source("maxTokens")}`);
|
|
944
|
+
lines.push(`reasoning: ${model.model.reasoning}${source("reasoning")}`);
|
|
945
|
+
lines.push(`input: ${model.model.input.join(", ")}${source("input")}`);
|
|
946
|
+
lines.push(`cost: $${model.model.cost.input}/$${model.model.cost.output} per Mtok${source("cost")}`);
|
|
947
|
+
if (effective !== void 0 && diverges(effective, model)) {
|
|
948
|
+
lines.push("");
|
|
949
|
+
lines.push("Pi is using different values (models.json modelOverrides win over this extension):");
|
|
950
|
+
lines.push(` context: ${effective.contextWindow} maxTokens: ${effective.maxTokens}`);
|
|
951
|
+
lines.push(` reasoning: ${effective.reasoning} cost: $${effective.cost.input}/$${effective.cost.output}`);
|
|
952
|
+
}
|
|
953
|
+
if (model.issues.length > 0) {
|
|
954
|
+
lines.push("");
|
|
955
|
+
for (const issue of model.issues) lines.push(`note: ${issue}`);
|
|
956
|
+
}
|
|
957
|
+
if (matches.length > 1) {
|
|
958
|
+
lines.push("");
|
|
959
|
+
lines.push(`'${modelId}' also exists on: ${matches.slice(1).map((match) => match.report.provider).join(", ")}`);
|
|
960
|
+
}
|
|
961
|
+
return lines.join("\n");
|
|
962
|
+
}
|
|
963
|
+
function diverges(effective, report) {
|
|
964
|
+
return effective.contextWindow !== report.model.contextWindow || effective.maxTokens !== report.model.maxTokens || effective.reasoning !== report.model.reasoning || effective.cost.input !== report.model.cost.input || effective.cost.output !== report.model.cost.output;
|
|
965
|
+
}
|
|
966
|
+
/** Runs on every keystroke, so it stays a prefix filter over an already-built list. */
|
|
967
|
+
function buildCompletions(reports, prefix) {
|
|
968
|
+
const needle = prefix.trim().toLowerCase();
|
|
969
|
+
const items = [];
|
|
970
|
+
if ("refresh".startsWith(needle)) items.push({
|
|
971
|
+
value: "refresh",
|
|
972
|
+
label: "refresh",
|
|
973
|
+
description: "Re-check the catalogs now"
|
|
974
|
+
});
|
|
975
|
+
for (const report of reports) for (const model of report.models) {
|
|
976
|
+
const value = `${report.provider}/${model.id}`;
|
|
977
|
+
if (needle.length === 0 || value.toLowerCase().includes(needle)) items.push({
|
|
978
|
+
value,
|
|
979
|
+
label: value,
|
|
980
|
+
description: model.resolution.kind
|
|
981
|
+
});
|
|
982
|
+
if (items.length >= COMPLETION_LIMIT) return items;
|
|
983
|
+
}
|
|
984
|
+
return items.length > 0 ? items : null;
|
|
985
|
+
}
|
|
986
|
+
function registerModelInfoCommand(pi, controller) {
|
|
987
|
+
try {
|
|
988
|
+
pi.registerCommand(COMMAND_NAME, {
|
|
989
|
+
description: "Inspect the model metadata pi-model-info resolved for your third-party providers",
|
|
990
|
+
getArgumentCompletions(prefix) {
|
|
991
|
+
return buildCompletions(controller.getReports(), prefix);
|
|
992
|
+
},
|
|
993
|
+
async handler(args, ctx) {
|
|
994
|
+
const argument = args.trim();
|
|
995
|
+
if (argument === "refresh") await controller.refresh();
|
|
996
|
+
if (argument === "refresh" || argument.length === 0) {
|
|
997
|
+
const summary = formatSummary(controller.getReports(), controller.getCatalog(), controller.getIssues(), Date.now());
|
|
998
|
+
ctx.ui.notify(summary, "info");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const { providerId, modelId } = splitReference(argument);
|
|
1002
|
+
const effective = providerId === void 0 ? void 0 : controller.getEffectiveModel(providerId, modelId);
|
|
1003
|
+
ctx.ui.notify(formatDetail(controller.getReports(), argument, effective), "info");
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
console.warn(`[pi-model-info] could not register /${COMMAND_NAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
//#endregion
|
|
1012
|
+
//#region src/models-json.ts
|
|
1013
|
+
/** Only fields this extension would otherwise overwrite are worth tracking. */
|
|
1014
|
+
const TRACKED_FIELDS = /* @__PURE__ */ new Set([
|
|
1015
|
+
"name",
|
|
1016
|
+
"reasoning",
|
|
1017
|
+
"input",
|
|
1018
|
+
"cost",
|
|
1019
|
+
"contextWindow",
|
|
1020
|
+
"maxTokens",
|
|
1021
|
+
"thinkingLevelMap"
|
|
1022
|
+
]);
|
|
1023
|
+
function isRecord(value) {
|
|
1024
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1025
|
+
}
|
|
1026
|
+
function defaultRead(path) {
|
|
1027
|
+
try {
|
|
1028
|
+
return readFileSync(path, "utf8");
|
|
1029
|
+
} catch {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Answers what the registry cannot: whether a value was hand-written or is Pi's placeholder.
|
|
1035
|
+
* Without it, a user who wrote `contextWindow: 200000` would silently get the catalog's number.
|
|
1036
|
+
*/
|
|
1037
|
+
function readUserAuthoredFields(agentDir, readFile = defaultRead) {
|
|
1038
|
+
const authored = /* @__PURE__ */ new Map();
|
|
1039
|
+
let raw;
|
|
1040
|
+
try {
|
|
1041
|
+
raw = readFile(join(agentDir, "models.json"));
|
|
1042
|
+
} catch {
|
|
1043
|
+
return authored;
|
|
1044
|
+
}
|
|
1045
|
+
if (raw === void 0) return authored;
|
|
1046
|
+
let parsed;
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = JSON.parse(raw);
|
|
1049
|
+
} catch {
|
|
1050
|
+
return authored;
|
|
1051
|
+
}
|
|
1052
|
+
if (!isRecord(parsed) || !isRecord(parsed["providers"])) return authored;
|
|
1053
|
+
for (const [providerId, provider] of Object.entries(parsed["providers"])) {
|
|
1054
|
+
if (!isRecord(provider) || !Array.isArray(provider["models"])) continue;
|
|
1055
|
+
const models = /* @__PURE__ */ new Map();
|
|
1056
|
+
for (const definition of provider["models"]) {
|
|
1057
|
+
if (!isRecord(definition) || typeof definition["id"] !== "string") continue;
|
|
1058
|
+
const fields = new Set(Object.keys(definition).filter((key) => TRACKED_FIELDS.has(key)));
|
|
1059
|
+
if (fields.size > 0) models.set(definition["id"], fields);
|
|
1060
|
+
}
|
|
1061
|
+
if (models.size > 0) authored.set(providerId, models);
|
|
1062
|
+
}
|
|
1063
|
+
return authored;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
//#endregion
|
|
1067
|
+
//#region src/merge.ts
|
|
1068
|
+
const OVERRIDE_FIELDS = [
|
|
1069
|
+
"name",
|
|
1070
|
+
"reasoning",
|
|
1071
|
+
"input",
|
|
1072
|
+
"cost",
|
|
1073
|
+
"contextWindow",
|
|
1074
|
+
"maxTokens",
|
|
1075
|
+
"thinkingLevelMap",
|
|
1076
|
+
"compat"
|
|
1077
|
+
];
|
|
1078
|
+
function isOverrideField(field) {
|
|
1079
|
+
return OVERRIDE_FIELDS.includes(field);
|
|
1080
|
+
}
|
|
1081
|
+
function mergeCost(base, incoming) {
|
|
1082
|
+
return compact({
|
|
1083
|
+
input: incoming.input ?? base.input,
|
|
1084
|
+
output: incoming.output ?? base.output,
|
|
1085
|
+
cacheRead: incoming.cacheRead ?? base.cacheRead,
|
|
1086
|
+
cacheWrite: incoming.cacheWrite ?? base.cacheWrite,
|
|
1087
|
+
tiers: incoming.tiers ?? base.tiers
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
function unionInput(a, b) {
|
|
1091
|
+
const combined = /* @__PURE__ */ new Set([...a, ...b]);
|
|
1092
|
+
const input = ["text"];
|
|
1093
|
+
if (combined.has("image")) input.push("image");
|
|
1094
|
+
return input;
|
|
1095
|
+
}
|
|
1096
|
+
/** A relay's markup applies to catalog pricing only, never to a rule's explicit `0`. */
|
|
1097
|
+
function scaleCost(cost, multiplier) {
|
|
1098
|
+
if (multiplier === 1) return cost;
|
|
1099
|
+
const scale = (value) => value === void 0 ? void 0 : value * multiplier;
|
|
1100
|
+
return compact({
|
|
1101
|
+
input: scale(cost.input),
|
|
1102
|
+
output: scale(cost.output),
|
|
1103
|
+
cacheRead: scale(cost.cacheRead),
|
|
1104
|
+
cacheWrite: scale(cost.cacheWrite),
|
|
1105
|
+
tiers: cost.tiers?.map((tier) => ({
|
|
1106
|
+
input: tier.input * multiplier,
|
|
1107
|
+
output: tier.output * multiplier,
|
|
1108
|
+
cacheRead: tier.cacheRead * multiplier,
|
|
1109
|
+
cacheWrite: tier.cacheWrite * multiplier,
|
|
1110
|
+
inputTokensAbove: tier.inputTokensAbove
|
|
1111
|
+
}))
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Cost, limits and capabilities all come from the single winning entry. Only fields the winner's
|
|
1116
|
+
* source schema cannot express are backfilled, and only from a same-provider pi.dev donor.
|
|
1117
|
+
*/
|
|
1118
|
+
function catalogLayer(match, provider, snapshot) {
|
|
1119
|
+
const { entry, donor } = match;
|
|
1120
|
+
const metadata = entry.metadata;
|
|
1121
|
+
const ownMap = entry.source === "pi.dev" || provider.mapThinkingLevels ? metadata.thinkingLevelMap : void 0;
|
|
1122
|
+
const layer = {};
|
|
1123
|
+
if (provider.useCatalogName && metadata.name !== void 0) layer.name = metadata.name;
|
|
1124
|
+
if (provider.capabilityPolicy !== "keep") {
|
|
1125
|
+
if (metadata.reasoning !== void 0) layer.reasoning = provider.capabilityPolicy === "widen" ? snapshot.reasoning || metadata.reasoning : metadata.reasoning;
|
|
1126
|
+
if (metadata.input !== void 0) layer.input = provider.capabilityPolicy === "widen" ? unionInput(snapshot.input, metadata.input) : metadata.input;
|
|
1127
|
+
}
|
|
1128
|
+
if (provider.contextWindowPolicy !== "keep") {
|
|
1129
|
+
if (metadata.contextWindow !== void 0) layer.contextWindow = provider.contextWindowPolicy === "min" ? Math.min(metadata.contextWindow, snapshot.contextWindow) : metadata.contextWindow;
|
|
1130
|
+
if (metadata.maxTokens !== void 0) layer.maxTokens = provider.contextWindowPolicy === "min" ? Math.min(metadata.maxTokens, snapshot.maxTokens) : metadata.maxTokens;
|
|
1131
|
+
}
|
|
1132
|
+
if (provider.costPolicy === "zero") layer.cost = {
|
|
1133
|
+
input: 0,
|
|
1134
|
+
output: 0,
|
|
1135
|
+
cacheRead: 0,
|
|
1136
|
+
cacheWrite: 0
|
|
1137
|
+
};
|
|
1138
|
+
else if (provider.costPolicy === "catalog" && metadata.cost !== void 0) layer.cost = scaleCost(metadata.cost, provider.costMultiplier);
|
|
1139
|
+
const thinkingLevelMap = ownMap ?? donor?.metadata.thinkingLevelMap;
|
|
1140
|
+
if (thinkingLevelMap !== void 0) layer.thinkingLevelMap = thinkingLevelMap;
|
|
1141
|
+
const compat = (entry.api === snapshot.api ? metadata.compat : void 0) ?? (donor?.api === snapshot.api ? donor.metadata.compat : void 0);
|
|
1142
|
+
if (compat !== void 0) layer.compat = compat;
|
|
1143
|
+
return layer;
|
|
1144
|
+
}
|
|
1145
|
+
/** Drops slots that are present but undefined; omission already means "provider default". */
|
|
1146
|
+
function compactThinkingLevelMap(map) {
|
|
1147
|
+
if (map === void 0) return;
|
|
1148
|
+
const compacted = {};
|
|
1149
|
+
let kept = false;
|
|
1150
|
+
for (const level of THINKING_LEVELS) {
|
|
1151
|
+
const value = map[level];
|
|
1152
|
+
if (value !== void 0) {
|
|
1153
|
+
compacted[level] = value;
|
|
1154
|
+
kept = true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return kept ? compacted : void 0;
|
|
1158
|
+
}
|
|
1159
|
+
function applyLayer(draft, label, override, provenance) {
|
|
1160
|
+
for (const field of OVERRIDE_FIELDS) if (override[field] !== void 0) provenance.set(field, label);
|
|
1161
|
+
if (override.name !== void 0) draft.name = override.name;
|
|
1162
|
+
if (override.reasoning !== void 0) draft.reasoning = override.reasoning;
|
|
1163
|
+
if (override.input !== void 0) draft.input = override.input;
|
|
1164
|
+
if (override.cost !== void 0) draft.cost = mergeCost(draft.cost, override.cost);
|
|
1165
|
+
if (override.contextWindow !== void 0) draft.contextWindow = override.contextWindow;
|
|
1166
|
+
if (override.maxTokens !== void 0) draft.maxTokens = override.maxTokens;
|
|
1167
|
+
if (override.thinkingLevelMap !== void 0) draft.thinkingLevelMap = {
|
|
1168
|
+
...draft.thinkingLevelMap,
|
|
1169
|
+
...override.thinkingLevelMap
|
|
1170
|
+
};
|
|
1171
|
+
if (override.compat !== void 0) draft.compat = override.compat;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Automatic values never overwrite what the user hand-wrote in models.json; an explicit override
|
|
1175
|
+
* layered later is a different matter, being equally deliberate.
|
|
1176
|
+
*/
|
|
1177
|
+
function dropAuthored(layer, authored, provenance) {
|
|
1178
|
+
for (const field of authored) if (isOverrideField(field)) {
|
|
1179
|
+
delete layer[field];
|
|
1180
|
+
provenance.set(field, "models.json");
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function isPositiveInt(value) {
|
|
1184
|
+
return Number.isInteger(value) && value > 0;
|
|
1185
|
+
}
|
|
1186
|
+
function costIsValid(cost) {
|
|
1187
|
+
return [
|
|
1188
|
+
cost.input,
|
|
1189
|
+
cost.output,
|
|
1190
|
+
cost.cacheRead,
|
|
1191
|
+
cost.cacheWrite,
|
|
1192
|
+
...(cost.tiers ?? []).flatMap((tier) => [
|
|
1193
|
+
tier.input,
|
|
1194
|
+
tier.output,
|
|
1195
|
+
tier.cacheRead,
|
|
1196
|
+
tier.cacheWrite
|
|
1197
|
+
])
|
|
1198
|
+
].every((value) => Number.isFinite(value) && value >= 0);
|
|
1199
|
+
}
|
|
1200
|
+
function mergeMetadata(input) {
|
|
1201
|
+
const { snapshot, provider, resolution, gate } = input;
|
|
1202
|
+
const issues = [];
|
|
1203
|
+
const provenance = new Map(OVERRIDE_FIELDS.map((field) => [field, "existing"]));
|
|
1204
|
+
const draft = {
|
|
1205
|
+
name: snapshot.name,
|
|
1206
|
+
reasoning: snapshot.reasoning,
|
|
1207
|
+
input: [...snapshot.input],
|
|
1208
|
+
cost: snapshot.cost,
|
|
1209
|
+
contextWindow: snapshot.contextWindow,
|
|
1210
|
+
maxTokens: snapshot.maxTokens,
|
|
1211
|
+
thinkingLevelMap: snapshot.thinkingLevelMap,
|
|
1212
|
+
compat: snapshot.compat
|
|
1213
|
+
};
|
|
1214
|
+
if (resolution.kind === "resolved") {
|
|
1215
|
+
const layer = catalogLayer(resolution, provider, snapshot);
|
|
1216
|
+
if (input.userAuthored !== void 0) dropAuthored(layer, input.userAuthored, provenance);
|
|
1217
|
+
applyLayer(draft, resolution.entry.source, layer, provenance);
|
|
1218
|
+
for (const rule of [resolution.prefixRule, resolution.suffixRule]) if (rule?.override !== void 0) applyLayer(draft, `rule '${rule.id}'`, rule.override, provenance);
|
|
1219
|
+
}
|
|
1220
|
+
if (gate?.override !== void 0) applyLayer(draft, "model override", gate.override, provenance);
|
|
1221
|
+
if (!isPositiveInt(draft.contextWindow)) {
|
|
1222
|
+
issues.push(`invalid contextWindow ${draft.contextWindow}; kept ${snapshot.contextWindow}`);
|
|
1223
|
+
draft.contextWindow = snapshot.contextWindow;
|
|
1224
|
+
provenance.set("contextWindow", "existing");
|
|
1225
|
+
}
|
|
1226
|
+
if (!isPositiveInt(draft.maxTokens)) {
|
|
1227
|
+
issues.push(`invalid maxTokens ${draft.maxTokens}; kept ${snapshot.maxTokens}`);
|
|
1228
|
+
draft.maxTokens = snapshot.maxTokens;
|
|
1229
|
+
provenance.set("maxTokens", "existing");
|
|
1230
|
+
}
|
|
1231
|
+
if (draft.maxTokens > draft.contextWindow) draft.maxTokens = draft.contextWindow;
|
|
1232
|
+
if (!costIsValid(draft.cost)) {
|
|
1233
|
+
issues.push("invalid cost; kept the existing rates");
|
|
1234
|
+
draft.cost = snapshot.cost;
|
|
1235
|
+
provenance.set("cost", "existing");
|
|
1236
|
+
}
|
|
1237
|
+
if (draft.input.length === 0) {
|
|
1238
|
+
draft.input = [...snapshot.input];
|
|
1239
|
+
provenance.set("input", "existing");
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
model: compact({
|
|
1243
|
+
id: snapshot.id,
|
|
1244
|
+
name: draft.name,
|
|
1245
|
+
api: snapshot.api,
|
|
1246
|
+
baseUrl: snapshot.baseUrl,
|
|
1247
|
+
reasoning: draft.reasoning,
|
|
1248
|
+
input: draft.input,
|
|
1249
|
+
cost: draft.cost,
|
|
1250
|
+
contextWindow: draft.contextWindow,
|
|
1251
|
+
maxTokens: draft.maxTokens,
|
|
1252
|
+
thinkingLevelMap: compactThinkingLevelMap(draft.thinkingLevelMap),
|
|
1253
|
+
compat: draft.compat,
|
|
1254
|
+
headers: snapshot.headers,
|
|
1255
|
+
samplingParams: snapshot.samplingParams
|
|
1256
|
+
}),
|
|
1257
|
+
issues,
|
|
1258
|
+
provenance
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/resolver.ts
|
|
1264
|
+
/**
|
|
1265
|
+
* Vendor qualification is a key form rather than an affix rule: relays overwhelmingly use
|
|
1266
|
+
* `vendor/model` ids, and spending the one-prefix budget on that would leave nothing for a real
|
|
1267
|
+
* prefix. The vendor-scoped form deliberately precedes the unscoped one, whose bare-id fallback can
|
|
1268
|
+
* return several providers — an explicit `anthropic/…` should settle that outright.
|
|
1269
|
+
*/
|
|
1270
|
+
function keyForms(provider, id) {
|
|
1271
|
+
const forms = [];
|
|
1272
|
+
const vendor = vendorOf(id);
|
|
1273
|
+
const rest = bareId(id);
|
|
1274
|
+
if (provider.catalogProvider !== void 0) {
|
|
1275
|
+
forms.push({
|
|
1276
|
+
scope: provider.catalogProvider,
|
|
1277
|
+
id,
|
|
1278
|
+
vendorScoped: false,
|
|
1279
|
+
vendor
|
|
1280
|
+
});
|
|
1281
|
+
if (vendor !== void 0) forms.push({
|
|
1282
|
+
scope: provider.catalogProvider,
|
|
1283
|
+
id: rest,
|
|
1284
|
+
vendorScoped: false,
|
|
1285
|
+
vendor
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
forms.push({
|
|
1289
|
+
scope: provider.id,
|
|
1290
|
+
id,
|
|
1291
|
+
vendorScoped: false,
|
|
1292
|
+
vendor
|
|
1293
|
+
});
|
|
1294
|
+
if (vendor !== void 0) forms.push({
|
|
1295
|
+
scope: vendor,
|
|
1296
|
+
id: rest,
|
|
1297
|
+
vendorScoped: true,
|
|
1298
|
+
vendor
|
|
1299
|
+
});
|
|
1300
|
+
forms.push({
|
|
1301
|
+
scope: void 0,
|
|
1302
|
+
id,
|
|
1303
|
+
vendorScoped: false,
|
|
1304
|
+
vendor
|
|
1305
|
+
});
|
|
1306
|
+
return forms;
|
|
1307
|
+
}
|
|
1308
|
+
function candidatesFor(index, form) {
|
|
1309
|
+
if (form.scope !== void 0) return {
|
|
1310
|
+
entries: index.scoped.get(scopedKey(form.scope, form.id)) ?? [],
|
|
1311
|
+
viaBare: false
|
|
1312
|
+
};
|
|
1313
|
+
const exact = index.exact.get(form.id.toLowerCase());
|
|
1314
|
+
if (exact !== void 0 && exact.length > 0) return {
|
|
1315
|
+
entries: exact,
|
|
1316
|
+
viaBare: false
|
|
1317
|
+
};
|
|
1318
|
+
return {
|
|
1319
|
+
entries: index.bare.get(bareId(form.id).toLowerCase()) ?? [],
|
|
1320
|
+
viaBare: true
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* The tie-break selects a PROVIDER; insertion order (already source-ranked) then selects the entry.
|
|
1325
|
+
* Splicing fields from two providers would be incoherent — the same bare id genuinely differs in
|
|
1326
|
+
* price and limits between them.
|
|
1327
|
+
*/
|
|
1328
|
+
function select(found, index, provider, form) {
|
|
1329
|
+
const candidates = found.entries;
|
|
1330
|
+
if (candidates.length === 0) return;
|
|
1331
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1332
|
+
for (const candidate of candidates) {
|
|
1333
|
+
const key = (candidate.sourceProvider ?? "").toLowerCase();
|
|
1334
|
+
const bucket = groups.get(key);
|
|
1335
|
+
if (bucket === void 0) groups.set(key, [candidate]);
|
|
1336
|
+
else bucket.push(candidate);
|
|
1337
|
+
}
|
|
1338
|
+
let group = groups.size === 1 ? [...groups.values()][0] : void 0;
|
|
1339
|
+
if (group === void 0) {
|
|
1340
|
+
const tiers = [
|
|
1341
|
+
provider.catalogProvider,
|
|
1342
|
+
provider.id,
|
|
1343
|
+
index.vendors.get(bareId(form.id).toLowerCase())
|
|
1344
|
+
];
|
|
1345
|
+
for (const tier of tiers) {
|
|
1346
|
+
const match = tier === void 0 ? void 0 : groups.get(tier.toLowerCase());
|
|
1347
|
+
if (match !== void 0) {
|
|
1348
|
+
group = match;
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (group === void 0) return { ambiguous: candidates };
|
|
1354
|
+
const winner = group[0];
|
|
1355
|
+
if (winner === void 0) return;
|
|
1356
|
+
return { hit: {
|
|
1357
|
+
entry: winner,
|
|
1358
|
+
donor: group.find((entry) => entry !== winner && entry.source === "pi.dev"),
|
|
1359
|
+
viaVendorSplit: form.vendorScoped || found.viaBare && form.vendor !== void 0
|
|
1360
|
+
} };
|
|
1361
|
+
}
|
|
1362
|
+
function lookup(index, provider, id) {
|
|
1363
|
+
for (const form of keyForms(provider, id)) {
|
|
1364
|
+
const outcome = select(candidatesFor(index, form), index, provider, form);
|
|
1365
|
+
if (outcome !== void 0) return outcome;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function matched(hit, matchKind, prefixRule, suffixRule) {
|
|
1369
|
+
return {
|
|
1370
|
+
kind: "resolved",
|
|
1371
|
+
entry: hit.entry,
|
|
1372
|
+
donor: hit.donor,
|
|
1373
|
+
matchKind,
|
|
1374
|
+
prefixRule,
|
|
1375
|
+
suffixRule
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
/** Removes at most one prefix and one suffix; a no-op or empty residual is not a match. */
|
|
1379
|
+
function strip(id, prefix, suffix) {
|
|
1380
|
+
let out = id;
|
|
1381
|
+
if (prefix !== void 0) {
|
|
1382
|
+
if (!out.startsWith(prefix.value)) return;
|
|
1383
|
+
out = out.slice(prefix.value.length);
|
|
1384
|
+
}
|
|
1385
|
+
if (suffix !== void 0) {
|
|
1386
|
+
if (!out.endsWith(suffix.value)) return;
|
|
1387
|
+
out = out.slice(0, out.length - suffix.value.length);
|
|
1388
|
+
}
|
|
1389
|
+
return out.length === 0 || out === id ? void 0 : out;
|
|
1390
|
+
}
|
|
1391
|
+
/** unset = every rule · `[]` = none · `['id']` = only those. */
|
|
1392
|
+
function gateRules(allowed, rules) {
|
|
1393
|
+
if (allowed === void 0) return rules;
|
|
1394
|
+
const ids = new Set(allowed);
|
|
1395
|
+
return rules.filter((rule) => ids.has(rule.id));
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Single strips before double strips, so `x-preview-free` does not lose `-preview` when only `-free`
|
|
1399
|
+
* was needed. Rules arrive longest-value-first, so a broad `-free` cannot shadow `-preview-free`.
|
|
1400
|
+
*/
|
|
1401
|
+
function combinations(prefixes, suffixes) {
|
|
1402
|
+
const combos = [];
|
|
1403
|
+
for (const suffix of suffixes) combos.push([void 0, suffix]);
|
|
1404
|
+
for (const prefix of prefixes) combos.push([prefix, void 0]);
|
|
1405
|
+
for (const prefix of prefixes) for (const suffix of suffixes) combos.push([prefix, suffix]);
|
|
1406
|
+
return combos;
|
|
1407
|
+
}
|
|
1408
|
+
function resolveModel(input) {
|
|
1409
|
+
const { index, provider, modelId } = input;
|
|
1410
|
+
const gate = provider.models.get(modelId);
|
|
1411
|
+
if (gate?.skip === true) return {
|
|
1412
|
+
kind: "unresolved",
|
|
1413
|
+
reason: "skipped"
|
|
1414
|
+
};
|
|
1415
|
+
if (gate?.alias !== void 0) {
|
|
1416
|
+
const outcome = lookup(index, provider, gate.alias);
|
|
1417
|
+
if (outcome === void 0) return {
|
|
1418
|
+
kind: "unresolved",
|
|
1419
|
+
reason: "alias-miss"
|
|
1420
|
+
};
|
|
1421
|
+
return "ambiguous" in outcome ? {
|
|
1422
|
+
kind: "ambiguous",
|
|
1423
|
+
candidates: outcome.ambiguous
|
|
1424
|
+
} : matched(outcome.hit, "alias");
|
|
1425
|
+
}
|
|
1426
|
+
const direct = lookup(index, provider, modelId);
|
|
1427
|
+
if (direct !== void 0) {
|
|
1428
|
+
if ("ambiguous" in direct) return {
|
|
1429
|
+
kind: "ambiguous",
|
|
1430
|
+
candidates: direct.ambiguous
|
|
1431
|
+
};
|
|
1432
|
+
return matched(direct.hit, direct.hit.viaVendorSplit ? "vendor-qualified" : "exact");
|
|
1433
|
+
}
|
|
1434
|
+
const combos = combinations(gateRules(gate?.prefixes, input.prefixRules), gateRules(gate?.suffixes, input.suffixRules));
|
|
1435
|
+
for (const [prefix, suffix] of combos) {
|
|
1436
|
+
const stripped = strip(modelId, prefix, suffix);
|
|
1437
|
+
if (stripped === void 0) continue;
|
|
1438
|
+
const outcome = lookup(index, provider, stripped);
|
|
1439
|
+
if (outcome === void 0) continue;
|
|
1440
|
+
return "ambiguous" in outcome ? {
|
|
1441
|
+
kind: "ambiguous",
|
|
1442
|
+
candidates: outcome.ambiguous
|
|
1443
|
+
} : matched(outcome.hit, "stripped", prefix, suffix);
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
kind: "unresolved",
|
|
1447
|
+
reason: (input.prefixRules.length > 0 || input.suffixRules.length > 0) && combos.length === 0 ? "rules-disabled" : "no-match"
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/provider-apply.ts
|
|
1453
|
+
var ProviderApplier = class {
|
|
1454
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1455
|
+
skipped = /* @__PURE__ */ new Map();
|
|
1456
|
+
reports = /* @__PURE__ */ new Map();
|
|
1457
|
+
lastRegistered = /* @__PURE__ */ new Map();
|
|
1458
|
+
warn;
|
|
1459
|
+
constructor(deps = {}) {
|
|
1460
|
+
this.warn = deps.warn ?? (() => {});
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Must run before the first registration of the session: afterwards `getProvider(id).getModels()`
|
|
1464
|
+
* returns our own list, and re-deriving from it would fold every previous pass into the next one.
|
|
1465
|
+
*/
|
|
1466
|
+
capture(registry, config) {
|
|
1467
|
+
this.snapshots.clear();
|
|
1468
|
+
this.skipped.clear();
|
|
1469
|
+
this.reports.clear();
|
|
1470
|
+
for (const provider of config.providers.values()) {
|
|
1471
|
+
const live = registry.getProvider(provider.id);
|
|
1472
|
+
if (live === void 0) {
|
|
1473
|
+
this.skip(provider.id, "not present in Pi; check the provider id");
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (registry.getRegisteredNativeProvider(provider.id) !== void 0) {
|
|
1477
|
+
this.skip(provider.id, "another extension registered a native provider for this id");
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const snapshot = [...live.getModels()];
|
|
1481
|
+
if (snapshot.length === 0) {
|
|
1482
|
+
this.skip(provider.id, "no models to complete");
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (live.refreshModels !== void 0 && !provider.allowDynamic) this.warn(`provider '${provider.id}' refreshes its model list dynamically; completing it freezes newly discovered models until the next session`);
|
|
1486
|
+
this.snapshots.set(provider.id, snapshot);
|
|
1487
|
+
this.reports.set(provider.id, {
|
|
1488
|
+
provider: provider.id,
|
|
1489
|
+
status: "pending",
|
|
1490
|
+
reason: void 0,
|
|
1491
|
+
models: []
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
apply(pi, config, catalog, userAuthored) {
|
|
1496
|
+
if (catalog.status === "unavailable") return;
|
|
1497
|
+
for (const [providerId, snapshot] of this.snapshots) {
|
|
1498
|
+
const provider = config.providers.get(providerId);
|
|
1499
|
+
if (provider !== void 0) this.applyProvider(pi, provider, snapshot, config, catalog, userAuthored);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
applyProvider(pi, provider, snapshots, config, catalog, userAuthored) {
|
|
1503
|
+
const authored = userAuthored.get(provider.id);
|
|
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
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Cheap id-set comparison against the live list. Ordering already guarantees a discovery extension
|
|
1552
|
+
* registers before our first pass, so this only catches a third party changing the list mid-session.
|
|
1553
|
+
*/
|
|
1554
|
+
reconcile(registry) {
|
|
1555
|
+
let drifted = false;
|
|
1556
|
+
for (const providerId of this.snapshots.keys()) {
|
|
1557
|
+
const live = registry.getProvider(providerId);
|
|
1558
|
+
if (live === void 0) continue;
|
|
1559
|
+
const liveModels = [...live.getModels()];
|
|
1560
|
+
const registered = this.lastRegistered.get(providerId);
|
|
1561
|
+
if (liveModels.length === 0 || registered !== void 0 && sameIds(liveModels, registered)) continue;
|
|
1562
|
+
this.snapshots.set(providerId, liveModels);
|
|
1563
|
+
drifted = true;
|
|
1564
|
+
}
|
|
1565
|
+
return drifted;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* `extensionProviders` outlives a `/reload` while our in-memory state does not, so a registration
|
|
1569
|
+
* for a provider that is no longer opted in would linger and could make a later recompose delete
|
|
1570
|
+
* the provider outright.
|
|
1571
|
+
*/
|
|
1572
|
+
releaseStale(pi, registry, config) {
|
|
1573
|
+
for (const [providerId, models] of this.lastRegistered) {
|
|
1574
|
+
if (config.providers.has(providerId)) continue;
|
|
1575
|
+
if (!isSolelyOurRegistration(registry.getRegisteredProviderConfig(providerId), models)) continue;
|
|
1576
|
+
try {
|
|
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
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
getReports() {
|
|
1585
|
+
const reports = [...this.reports.values()];
|
|
1586
|
+
for (const [provider, reason] of this.skipped) reports.push({
|
|
1587
|
+
provider,
|
|
1588
|
+
status: "skipped",
|
|
1589
|
+
reason,
|
|
1590
|
+
models: []
|
|
1591
|
+
});
|
|
1592
|
+
return reports.sort((a, b) => a.provider.localeCompare(b.provider));
|
|
1593
|
+
}
|
|
1594
|
+
skip(providerId, reason) {
|
|
1595
|
+
this.skipped.set(providerId, reason);
|
|
1596
|
+
this.warn(`skipping provider '${providerId}': ${reason}`);
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
function sameIds(live, registered) {
|
|
1600
|
+
if (live.length !== registered.length) return false;
|
|
1601
|
+
const ids = new Set(registered.map((model) => model.id));
|
|
1602
|
+
return live.every((model) => ids.has(model.id));
|
|
1603
|
+
}
|
|
1604
|
+
function isSolelyOurRegistration(stored, models) {
|
|
1605
|
+
if (stored === void 0) return false;
|
|
1606
|
+
const keys = Object.keys(stored);
|
|
1607
|
+
return keys.length === 1 && keys[0] === "models" && stored.models === models;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
//#endregion
|
|
1611
|
+
//#region src/extension.ts
|
|
1612
|
+
function defaultWarn(message) {
|
|
1613
|
+
console.warn(`[${EXTENSION_ID}] ${message}`);
|
|
1614
|
+
}
|
|
1615
|
+
function createModelInfoExtension(pi, dependencies = {}) {
|
|
1616
|
+
const warn = dependencies.warn ?? defaultWarn;
|
|
1617
|
+
const agentDir = dependencies.agentDir ?? defaultModelInfoAgentDir();
|
|
1618
|
+
const loadConfig = dependencies.loadConfig ?? ((cwd, dir) => loadModelInfoConfig({
|
|
1619
|
+
cwd,
|
|
1620
|
+
agentDir: dir
|
|
1621
|
+
}));
|
|
1622
|
+
const readUserAuthored = dependencies.readUserAuthored ?? ((dir) => readUserAuthoredFields(dir));
|
|
1623
|
+
const schedule = dependencies.schedule ?? ((task) => {
|
|
1624
|
+
setTimeout(task, 0);
|
|
1625
|
+
});
|
|
1626
|
+
const store = dependencies.catalogStore ?? new CatalogStore();
|
|
1627
|
+
const applier = dependencies.applier ?? new ProviderApplier({ warn });
|
|
1628
|
+
let config;
|
|
1629
|
+
let issues = [];
|
|
1630
|
+
let registry;
|
|
1631
|
+
let userAuthored = /* @__PURE__ */ new Map();
|
|
1632
|
+
let catalog;
|
|
1633
|
+
let pending;
|
|
1634
|
+
let session;
|
|
1635
|
+
let isIdle = () => true;
|
|
1636
|
+
function applyCatalog(snapshot) {
|
|
1637
|
+
catalog = snapshot;
|
|
1638
|
+
if (config === void 0) return;
|
|
1639
|
+
if (config.applyOnIdleOnly && !isIdle()) {
|
|
1640
|
+
pending = snapshot;
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
pending = void 0;
|
|
1644
|
+
applier.apply(pi, config, snapshot, userAuthored);
|
|
1645
|
+
}
|
|
1646
|
+
async function run(signal) {
|
|
1647
|
+
if (config === void 0) return;
|
|
1648
|
+
applyCatalog(store.load(config));
|
|
1649
|
+
if (signal.aborted) return;
|
|
1650
|
+
const refreshed = await store.refresh(config, signal);
|
|
1651
|
+
if (!signal.aborted) applyCatalog(refreshed);
|
|
1652
|
+
}
|
|
1653
|
+
function start() {
|
|
1654
|
+
const controller = new AbortController();
|
|
1655
|
+
session = controller;
|
|
1656
|
+
schedule(() => {
|
|
1657
|
+
run(controller.signal).catch((error) => {
|
|
1658
|
+
warn(`catalog refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
pi.on("session_start", (_event, context) => {
|
|
1663
|
+
session?.abort();
|
|
1664
|
+
session = void 0;
|
|
1665
|
+
pending = void 0;
|
|
1666
|
+
catalog = void 0;
|
|
1667
|
+
registry = context.modelRegistry;
|
|
1668
|
+
isIdle = () => context.isIdle();
|
|
1669
|
+
const loaded = loadConfig(context.cwd, agentDir);
|
|
1670
|
+
const resolved = loaded.config === void 0 ? void 0 : resolveModelInfoConfig(loaded.config, loaded.globalPath);
|
|
1671
|
+
issues = [...loaded.issues, ...resolved?.issues ?? []];
|
|
1672
|
+
for (const issue of issues) warn(`config issue at ${issue.sourcePath}: ${issue.message}`);
|
|
1673
|
+
config = resolved?.config;
|
|
1674
|
+
if (config === void 0) return;
|
|
1675
|
+
applier.releaseStale(pi, context.modelRegistry, config);
|
|
1676
|
+
if (config.providers.size === 0) return;
|
|
1677
|
+
applier.capture(context.modelRegistry, config);
|
|
1678
|
+
userAuthored = readUserAuthored(agentDir);
|
|
1679
|
+
start();
|
|
1680
|
+
});
|
|
1681
|
+
pi.on("before_agent_start", (_event, context) => {
|
|
1682
|
+
if (config === void 0 || catalog === void 0 || registry === void 0) return;
|
|
1683
|
+
isIdle = () => context.isIdle();
|
|
1684
|
+
if (applier.reconcile(registry)) applier.apply(pi, config, catalog, userAuthored);
|
|
1685
|
+
});
|
|
1686
|
+
pi.on("turn_end", () => {
|
|
1687
|
+
if (pending !== void 0 && config !== void 0) applier.apply(pi, config, pending, userAuthored);
|
|
1688
|
+
pending = void 0;
|
|
1689
|
+
});
|
|
1690
|
+
pi.on("session_shutdown", () => {
|
|
1691
|
+
session?.abort();
|
|
1692
|
+
session = void 0;
|
|
1693
|
+
pending = void 0;
|
|
1694
|
+
});
|
|
1695
|
+
registerModelInfoCommand(pi, {
|
|
1696
|
+
getReports: () => applier.getReports(),
|
|
1697
|
+
getCatalog: () => catalog,
|
|
1698
|
+
getIssues: () => issues,
|
|
1699
|
+
getEffectiveModel: (providerId, modelId) => registry?.find(providerId, modelId),
|
|
1700
|
+
refresh: async () => {
|
|
1701
|
+
if (config === void 0) return;
|
|
1702
|
+
applyCatalog(await store.refresh(config, new AbortController().signal, true));
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
//#endregion
|
|
1708
|
+
//#region src/index.ts
|
|
1709
|
+
function modelInfoExtension(pi) {
|
|
1710
|
+
createModelInfoExtension(pi);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
//#endregion
|
|
1714
|
+
export { modelInfoExtension as default };
|
|
1715
|
+
//# sourceMappingURL=index.js.map
|