@linxiraos/pi-catalog 1.1.5 → 1.1.7
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/CHANGELOG.md +8 -0
- package/dist/types/discovery/cursor.d.ts +6 -0
- package/dist/types/identity/family.d.ts +15 -11
- package/dist/types/model-manager.d.ts +20 -4
- package/dist/types/provider-models/models-dev-policies.d.ts +8 -0
- package/dist/types/provider-models/openai-compat.d.ts +20 -4
- package/dist/types/wire/cloudflare-ai-gateway.d.ts +18 -0
- package/package.json +4 -4
- package/src/discovery/cursor.ts +26 -11
- package/src/identity/family.ts +43 -24
- package/src/model-manager.ts +112 -71
- package/src/model-thinking.ts +13 -2
- package/src/models.json +40109 -29220
- package/src/provider-models/cache-provider-id.ts +4 -1
- package/src/provider-models/models-dev-policies.ts +38 -0
- package/src/provider-models/openai-compat.ts +221 -72
- package/src/variant-collapse.ts +1 -1
- package/src/wire/cloudflare-ai-gateway.ts +44 -0
package/src/model-manager.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ModelsDevFallback<TApi extends Api = Api, TPayload = unknown> {
|
|
|
21
21
|
fetch(): Promise<TPayload>;
|
|
22
22
|
/** Maps payload into provider models. */
|
|
23
23
|
map(payload: TPayload, providerId: Provider): readonly ModelSpec<TApi>[];
|
|
24
|
+
/** When true, mapped rows can add model ids but cannot replace static metadata. */
|
|
25
|
+
additiveOnly?: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/**
|
|
@@ -57,18 +59,26 @@ export interface ModelManagerOptions<TApi extends Api = Api, TModelsDevPayload =
|
|
|
57
59
|
now?: () => number;
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Catalog source that most recently refreshed the resolved provider snapshot.
|
|
64
|
+
*/
|
|
65
|
+
export type ModelResolutionSource = "bundled" | "cache" | "models.dev" | "provider";
|
|
66
|
+
|
|
60
67
|
/**
|
|
61
68
|
* Resolution result.
|
|
62
69
|
*
|
|
63
70
|
* `stale` is false when the resolved catalog is authoritative for the selected provider:
|
|
64
|
-
* - a
|
|
71
|
+
* - a provider endpoint fetch succeeded in this call (an empty catalog is still
|
|
65
72
|
* authoritative for the cycle, so downstream pruning of removed models runs),
|
|
73
|
+
* - a models.dev fetch succeeded for a provider without endpoint discovery,
|
|
66
74
|
* - a still-fresh authoritative cache was reused in `online-if-uncached` mode, or
|
|
67
|
-
* - the provider has no
|
|
75
|
+
* - the provider has no remote fetcher configured.
|
|
68
76
|
*/
|
|
69
77
|
export interface ModelResolutionResult<TApi extends Api = Api> {
|
|
70
78
|
models: Model<TApi>[];
|
|
71
79
|
stale: boolean;
|
|
80
|
+
source: ModelResolutionSource;
|
|
81
|
+
updatedAt?: number;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
/**
|
|
@@ -171,9 +181,10 @@ function restoreCachedModelHeaders<TApi extends Api>(
|
|
|
171
181
|
|
|
172
182
|
/**
|
|
173
183
|
* Resolves provider models with source precedence:
|
|
174
|
-
* static -> stencil.so ->
|
|
184
|
+
* static -> cached fallback -> stencil.so -> dynamic.
|
|
175
185
|
*
|
|
176
|
-
* Later sources override earlier ones by model id.
|
|
186
|
+
* Later sources override earlier ones by model id. Cached rows participate only
|
|
187
|
+
* when at least one configured remote source did not refresh successfully.
|
|
177
188
|
*/
|
|
178
189
|
export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPayload = unknown>(
|
|
179
190
|
options: ModelManagerOptions<TApi, TModelsDevPayload>,
|
|
@@ -187,6 +198,10 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
187
198
|
const staticModels = options.staticModels
|
|
188
199
|
? passModelList<TApi>(options.staticModels)
|
|
189
200
|
: (getBundledModels(options.providerId as GeneratedProvider) as Model<TApi>[]);
|
|
201
|
+
const additiveStaticModelIds =
|
|
202
|
+
options.modelsDev?.additiveOnly && staticModels.length > 0
|
|
203
|
+
? new Set(staticModels.map(model => model.id))
|
|
204
|
+
: undefined;
|
|
190
205
|
const cache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
|
|
191
206
|
const restoredCache = restoreCachedModelHeaders(
|
|
192
207
|
cache?.models ?? [],
|
|
@@ -200,7 +215,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
200
215
|
const cacheHasUnresolvedHeaders = restoredCache.unresolvedModelIds.size > 0;
|
|
201
216
|
const dynamicModelsAuthoritative = options.dynamicModelsAuthoritative ?? false;
|
|
202
217
|
const cacheDropIds = options.dropCachedModelIdsOnStaticMismatch;
|
|
203
|
-
const staticCatalogFingerprint =
|
|
218
|
+
const staticCatalogFingerprint = fingerprintStaticModels(staticModels, dynamicModelsAuthoritative);
|
|
204
219
|
// Endpoint-migration policy is cache identity: adding an id must invalidate
|
|
205
220
|
// matching-static-catalog caches written by the prior resolver.
|
|
206
221
|
const staticFingerprint =
|
|
@@ -219,20 +234,18 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
219
234
|
(!dynamicModelsAuthoritative || cacheFingerprintMatches);
|
|
220
235
|
const dynamicFetcher = options.fetchDynamicModels;
|
|
221
236
|
const hasDynamicFetcher = typeof dynamicFetcher === "function";
|
|
222
|
-
const
|
|
237
|
+
const hasModelsDevFetcher = options.modelsDev !== undefined;
|
|
238
|
+
const hasRemoteFetcher = hasDynamicFetcher || hasModelsDevFetcher;
|
|
239
|
+
const hasAuthoritativeCache = ((cache?.authoritative ?? false) && hasUsableFreshCache) || !hasRemoteFetcher;
|
|
223
240
|
const cacheAgeMs = cache ? now() - cache.updatedAt : Number.POSITIVE_INFINITY;
|
|
224
|
-
const shouldFetchFromNetwork =
|
|
225
|
-
strategy,
|
|
226
|
-
hasUsableFreshCache,
|
|
227
|
-
hasAuthoritativeCache,
|
|
228
|
-
cacheAgeMs,
|
|
229
|
-
);
|
|
241
|
+
const shouldFetchFromNetwork =
|
|
242
|
+
hasRemoteFetcher && shouldFetchRemoteSources(strategy, hasUsableFreshCache, hasAuthoritativeCache, cacheAgeMs);
|
|
230
243
|
|
|
231
244
|
// Cold-start fast path: when a fresh, authoritative cache exists, the network
|
|
232
245
|
// fetch is skipped, AND the static catalog slice is byte-identical to what
|
|
233
246
|
// was merged in last time, the cache row IS the authoritative merge result.
|
|
234
|
-
//
|
|
235
|
-
//
|
|
247
|
+
// Additive caches still need same-id rows stripped because an older binary
|
|
248
|
+
// may have written the snapshot before additive semantics were enabled.
|
|
236
249
|
if (
|
|
237
250
|
!shouldFetchFromNetwork &&
|
|
238
251
|
cache?.fresh &&
|
|
@@ -240,17 +253,40 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
240
253
|
cacheFingerprintMatches &&
|
|
241
254
|
!cacheHasUnresolvedHeaders
|
|
242
255
|
) {
|
|
243
|
-
|
|
256
|
+
const cacheContribution = additiveStaticModelIds
|
|
257
|
+
? restoredCache.models.filter(model => !additiveStaticModelIds.has(model.id))
|
|
258
|
+
: restoredCache.models;
|
|
259
|
+
const cachedModels = additiveStaticModelIds
|
|
260
|
+
? mergeDynamicModels(staticModels, cacheContribution)
|
|
261
|
+
: restoredCache.models;
|
|
262
|
+
const source: ModelResolutionSource = cacheContribution.length > 0 ? "cache" : "bundled";
|
|
263
|
+
return {
|
|
264
|
+
models: collapseBuiltModelVariants(cachedModels),
|
|
265
|
+
stale: false,
|
|
266
|
+
source,
|
|
267
|
+
...(source === "cache" ? { updatedAt: cache.updatedAt } : {}),
|
|
268
|
+
};
|
|
244
269
|
}
|
|
245
270
|
|
|
246
271
|
const [fetchedModelsDevModels, fetchedDynamicModels] = shouldFetchFromNetwork
|
|
247
272
|
? await Promise.all([fetchModelsDev(options), dynamicFetcher ? fetchDynamicModels(dynamicFetcher) : null])
|
|
248
273
|
: [null, null];
|
|
249
|
-
const
|
|
274
|
+
const modelsDevFetchSucceeded = fetchedModelsDevModels !== null;
|
|
275
|
+
const normalizedModelsDevModels = normalizeModelList<TApi>(fetchedModelsDevModels ?? []);
|
|
276
|
+
const modelsDevModels = additiveStaticModelIds
|
|
277
|
+
? normalizedModelsDevModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
278
|
+
: normalizedModelsDevModels;
|
|
250
279
|
const shouldUseFreshCacheAsAuthoritative =
|
|
251
280
|
strategy === "online-if-uncached" && hasUsableFreshCache && hasAuthoritativeCache;
|
|
252
281
|
const dynamicFetchSucceeded = fetchedDynamicModels !== null;
|
|
253
|
-
const
|
|
282
|
+
const anyRemoteFetchSucceeded = modelsDevFetchSucceeded || dynamicFetchSucceeded;
|
|
283
|
+
const allConfiguredRemoteFetchesSucceeded =
|
|
284
|
+
hasRemoteFetcher &&
|
|
285
|
+
(!hasModelsDevFetcher || modelsDevFetchSucceeded) &&
|
|
286
|
+
(!hasDynamicFetcher || dynamicFetchSucceeded);
|
|
287
|
+
const authoritativeDynamicFetchSucceeded = dynamicModelsAuthoritative && dynamicFetchSucceeded;
|
|
288
|
+
const remoteResolutionComplete = authoritativeDynamicFetchSucceeded || allConfiguredRemoteFetchesSucceeded;
|
|
289
|
+
const preparedCacheModels = remoteResolutionComplete
|
|
254
290
|
? []
|
|
255
291
|
: prepareCacheModelsForStaticMismatch(
|
|
256
292
|
usableCachedModels,
|
|
@@ -258,37 +294,44 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
258
294
|
cacheFingerprintMatches,
|
|
259
295
|
options.dropCachedModelIdsOnStaticMismatch,
|
|
260
296
|
);
|
|
297
|
+
// Additive shared-catalog rows may only introduce IDs. Apply that boundary
|
|
298
|
+
// to cache fallback too, including snapshots written by an older binary.
|
|
299
|
+
const cacheModels = additiveStaticModelIds
|
|
300
|
+
? preparedCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
301
|
+
: preparedCacheModels;
|
|
261
302
|
const dynamicModels = fetchedDynamicModels ?? [];
|
|
262
|
-
// A successful empty result stays authoritative for THIS cycle (so an
|
|
303
|
+
// A successful empty endpoint result stays authoritative for THIS cycle (so an
|
|
263
304
|
// intentional catalog emptying still prunes removed models downstream), but
|
|
264
305
|
// is NOT pinned into the cache as authoritative — that would suppress the
|
|
265
|
-
// short retry that recovers a transient empty response (#6620).
|
|
266
|
-
//
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
306
|
+
// short retry that recovers a transient empty response (#6620). Shared
|
|
307
|
+
// models.dev snapshots may be empty for one provider and remain authoritative.
|
|
308
|
+
const cacheAuthoritative = hasDynamicFetcher
|
|
309
|
+
? dynamicFetchSucceeded &&
|
|
310
|
+
dynamicModels.length > 0 &&
|
|
311
|
+
(dynamicModelsAuthoritative || !hasModelsDevFetcher || modelsDevFetchSucceeded)
|
|
312
|
+
: modelsDevFetchSucceeded;
|
|
313
|
+
const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
|
|
314
|
+
const mergedWithModelsDev = mergeDynamicModels(mergedWithCache, modelsDevModels);
|
|
315
|
+
const mergedModels = mergeDynamicModels(mergedWithModelsDev, dynamicModels);
|
|
270
316
|
const models = collapseBuiltModelVariants(
|
|
271
|
-
|
|
317
|
+
authoritativeDynamicFetchSucceeded ? retainModelIds(mergedModels, dynamicModels) : mergedModels,
|
|
272
318
|
);
|
|
273
|
-
const
|
|
319
|
+
const resolutionAuthoritative = !hasRemoteFetcher || remoteResolutionComplete || shouldUseFreshCacheAsAuthoritative;
|
|
320
|
+
const remoteUpdatedAt = anyRemoteFetchSucceeded ? now() : undefined;
|
|
274
321
|
if (shouldFetchFromNetwork) {
|
|
275
|
-
if (
|
|
276
|
-
const mergedSnapshot = mergeDynamicModels(mergeModelSources(staticModels, modelsDevModels), dynamicModels);
|
|
277
|
-
const snapshotModels = dynamicModelsAuthoritative
|
|
278
|
-
? retainModelIds(mergedSnapshot, dynamicModels)
|
|
279
|
-
: mergedSnapshot;
|
|
322
|
+
if (anyRemoteFetchSucceeded) {
|
|
280
323
|
writeModelCache(
|
|
281
324
|
cacheProviderId,
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
325
|
+
remoteUpdatedAt!,
|
|
326
|
+
models,
|
|
327
|
+
cacheAuthoritative,
|
|
285
328
|
staticFingerprint,
|
|
286
329
|
dbPath,
|
|
287
330
|
staticModels,
|
|
288
331
|
restorableHeaderFallback,
|
|
289
332
|
);
|
|
290
333
|
} else {
|
|
291
|
-
//
|
|
334
|
+
// Remote fetch failed — update cache with a non-authoritative snapshot so
|
|
292
335
|
// stale state remains visible while retry backoff still applies.
|
|
293
336
|
const latestCache = readModelCache<TApi>(cacheProviderId, ttlMs, now, dbPath);
|
|
294
337
|
const latestRestoredCache = restoreCachedModelHeaders(
|
|
@@ -302,16 +345,17 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
302
345
|
const latestUsableCacheModels = latestRestoredCache.models.filter(
|
|
303
346
|
model => !latestRestoredCache.unresolvedModelIds.has(model.id),
|
|
304
347
|
);
|
|
348
|
+
const preparedLatestCacheModels = prepareCacheModelsForStaticMismatch(
|
|
349
|
+
latestUsableCacheModels,
|
|
350
|
+
staticModels,
|
|
351
|
+
cacheFingerprintMatches,
|
|
352
|
+
options.dropCachedModelIdsOnStaticMismatch,
|
|
353
|
+
);
|
|
354
|
+
const latestCacheModels = additiveStaticModelIds
|
|
355
|
+
? preparedLatestCacheModels.filter(model => !additiveStaticModelIds.has(model.id))
|
|
356
|
+
: preparedLatestCacheModels;
|
|
305
357
|
const fallbackSnapshotModels = collapseBuiltModelVariants(
|
|
306
|
-
mergeDynamicModels(
|
|
307
|
-
mergeModelSources(staticModels, modelsDevModels),
|
|
308
|
-
prepareCacheModelsForStaticMismatch(
|
|
309
|
-
latestUsableCacheModels,
|
|
310
|
-
staticModels,
|
|
311
|
-
cacheFingerprintMatches,
|
|
312
|
-
options.dropCachedModelIdsOnStaticMismatch,
|
|
313
|
-
),
|
|
314
|
-
),
|
|
358
|
+
mergeDynamicModels(mergeDynamicModels(staticModels, latestCacheModels), modelsDevModels),
|
|
315
359
|
);
|
|
316
360
|
writeModelCache(
|
|
317
361
|
cacheProviderId,
|
|
@@ -325,9 +369,23 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
|
|
|
325
369
|
);
|
|
326
370
|
}
|
|
327
371
|
}
|
|
372
|
+
const cacheContributed = cacheModels.length > 0;
|
|
373
|
+
const source: ModelResolutionSource = dynamicFetchSucceeded
|
|
374
|
+
? "provider"
|
|
375
|
+
: modelsDevFetchSucceeded
|
|
376
|
+
? "models.dev"
|
|
377
|
+
: cacheContributed
|
|
378
|
+
? "cache"
|
|
379
|
+
: "bundled";
|
|
328
380
|
return {
|
|
329
381
|
models,
|
|
330
|
-
stale: !
|
|
382
|
+
stale: !resolutionAuthoritative,
|
|
383
|
+
source,
|
|
384
|
+
...(remoteUpdatedAt !== undefined
|
|
385
|
+
? { updatedAt: remoteUpdatedAt }
|
|
386
|
+
: cacheContributed && cache
|
|
387
|
+
? { updatedAt: cache.updatedAt }
|
|
388
|
+
: {}),
|
|
331
389
|
};
|
|
332
390
|
}
|
|
333
391
|
|
|
@@ -409,23 +467,6 @@ function prepareCacheModelsForStaticMismatch<TApi extends Api>(
|
|
|
409
467
|
return sanitizedModels;
|
|
410
468
|
}
|
|
411
469
|
|
|
412
|
-
function mergeModelSources<TApi extends Api>(...sources: readonly (readonly Model<TApi>[])[]): Model<TApi>[] {
|
|
413
|
-
// Strip out empty/missing sources up front. The hot path is `(static, [])`
|
|
414
|
-
// (modelsDev disabled / failed) — a single non-empty source means we can
|
|
415
|
-
// skip the Map churn entirely and just hand back the array.
|
|
416
|
-
const nonEmpty = sources.filter(source => source.length > 0);
|
|
417
|
-
if (nonEmpty.length === 0) return [];
|
|
418
|
-
if (nonEmpty.length === 1) return [...nonEmpty[0]];
|
|
419
|
-
const merged = new Map<string, Model<TApi>>();
|
|
420
|
-
for (const source of nonEmpty) {
|
|
421
|
-
for (const model of source) {
|
|
422
|
-
if (!model?.id) continue;
|
|
423
|
-
merged.set(model.id, model);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return Array.from(merged.values());
|
|
427
|
-
}
|
|
428
|
-
|
|
429
470
|
function mergeDynamicModels<TApi extends Api>(
|
|
430
471
|
baseModels: readonly Model<TApi>[],
|
|
431
472
|
dynamicModels: readonly Model<TApi>[],
|
|
@@ -459,22 +500,22 @@ function retainModelIds<TApi extends Api>(
|
|
|
459
500
|
return models.filter(model => retainedIds.has(model.id));
|
|
460
501
|
}
|
|
461
502
|
|
|
462
|
-
/**
|
|
463
|
-
* Stable, low-collision fingerprint of a static catalog slice. Cached by
|
|
464
|
-
* reference so repeat calls in the same process (e.g. multiple cold-start
|
|
465
|
-
* arms calling `resolveProviderModels` with the same `staticModels` array)
|
|
466
|
-
* skip the JSON+hash work after the first call.
|
|
467
|
-
*/
|
|
468
503
|
const MODEL_CACHE_FINGERPRINT_VERSION = "merge-v3";
|
|
469
504
|
const kStaticFingerprint = Symbol("model-manager.staticFingerprint");
|
|
470
|
-
type ModelArrayWithFingerprint = readonly Model<Api>[] & { [kStaticFingerprint]?: string };
|
|
471
|
-
|
|
472
|
-
|
|
505
|
+
type ModelArrayWithFingerprint = readonly (ModelSpec<Api> | Model<Api>)[] & { [kStaticFingerprint]?: string };
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Return the versioned, low-collision model-cache identity for a static provider
|
|
509
|
+
* slice. Results are cached by array reference so repeat cold-start paths skip
|
|
510
|
+
* the JSON serialization and hash.
|
|
511
|
+
*/
|
|
512
|
+
export function fingerprintStaticModels<TApi extends Api>(
|
|
513
|
+
models: readonly (ModelSpec<TApi> | Model<TApi>)[],
|
|
473
514
|
dynamicModelsAuthoritative = false,
|
|
474
515
|
): string {
|
|
475
516
|
if (models.length === 0) return `${MODEL_CACHE_FINGERPRINT_VERSION}:empty`;
|
|
476
517
|
if (dynamicModelsAuthoritative)
|
|
477
|
-
return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${
|
|
518
|
+
return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${fingerprintStaticModels(models)}`;
|
|
478
519
|
const tagged = models as ModelArrayWithFingerprint;
|
|
479
520
|
const cached = tagged[kStaticFingerprint];
|
|
480
521
|
if (cached !== undefined) return cached;
|
package/src/model-thinking.ts
CHANGED
|
@@ -429,11 +429,14 @@ function getModelDefinedEfforts<TApi extends Api>(
|
|
|
429
429
|
if (isDeepseekV4FlashModelId(spec.id)) {
|
|
430
430
|
return LOW_HIGH_MAX_REASONING_EFFORTS;
|
|
431
431
|
}
|
|
432
|
-
|
|
432
|
+
const bareId = bareModelId(spec.id).toLowerCase();
|
|
433
|
+
if (bareId.includes("deepseek-v4")) {
|
|
433
434
|
if (!isOpenRouterThinkingFormat(compat)) {
|
|
434
435
|
return LOW_HIGH_MAX_REASONING_EFFORTS;
|
|
435
436
|
}
|
|
436
|
-
|
|
437
|
+
// Route suffixes are OpenRouter-specific and not part of the model identity.
|
|
438
|
+
const openRouterBaseId = bareId.replace(/:[^:]+$/, "");
|
|
439
|
+
return openRouterBaseId === "deepseek-v4-pro-0813"
|
|
437
440
|
? LOW_HIGH_MAX_REASONING_EFFORTS
|
|
438
441
|
: HIGH_ONLY_REASONING_EFFORTS;
|
|
439
442
|
}
|
|
@@ -777,6 +780,14 @@ function inferThinkingControlMode<TApi extends Api>(
|
|
|
777
780
|
return "anthropic-budget-effort";
|
|
778
781
|
}
|
|
779
782
|
}
|
|
783
|
+
// Bedrock serves the GPT-5.x models through OpenAI's own request
|
|
784
|
+
// schema, which rejects Anthropic's budget block outright:
|
|
785
|
+
// `unknown_parameter: 'thinking'`. It takes `reasoning.effort`
|
|
786
|
+
// instead. gpt-oss parses as `unknown` (no `gpt-<digits>`), so it
|
|
787
|
+
// keeps the budget path it ships with today.
|
|
788
|
+
if (parsedModel.family === "openai") {
|
|
789
|
+
return "effort";
|
|
790
|
+
}
|
|
780
791
|
return "budget";
|
|
781
792
|
|
|
782
793
|
default:
|