@oh-my-pi/pi-catalog 18.1.5 → 18.1.6

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.1.6] - 2026-09-03
6
+
7
+ ### Added
8
+
9
+ - Added catalog-delivered model intelligence scores and estimated output throughput to help compare model capabilities and performance.
10
+
11
+ ### Changed
12
+
13
+ - Improved model search and selection so configured roles, provider preferences, and recent usage are prioritized while browsing and filtering models.
14
+
5
15
  ## [18.1.5] - 2026-09-03
6
16
 
7
17
  ### Added
@@ -0,0 +1,28 @@
1
+ import type { Api, Model } from "../types.js";
2
+ /** Catalog-delivered intelligence score and output speed for one model. */
3
+ export interface CatalogMetrics {
4
+ int?: number;
5
+ tps?: number;
6
+ }
7
+ /** The catalog metrics a model carries, or undefined when it reports none. A zero speed is "unmeasured", not a score. */
8
+ export declare function catalogMetricsOf(model: Model<Api>): CatalogMetrics | undefined;
9
+ /**
10
+ * Index of catalog metrics over every scored model seen so far. Built once per
11
+ * discovery cycle by the model registry and per provider by the model manager;
12
+ * `add` accumulates across providers so a proxy id resolves against any host's
13
+ * scored row.
14
+ */
15
+ export declare class CatalogMetricsIndex {
16
+ #private;
17
+ constructor(models?: Iterable<Model<Api>>);
18
+ get isEmpty(): boolean;
19
+ /** Record the metrics of every scored model; later rows fill fields earlier rows left unset. */
20
+ add(models: Iterable<Model<Api>>): void;
21
+ /** Metrics for `model` by exact id, else by dialect-normalized id when the classified identities agree. */
22
+ resolve(model: Model<Api>): CatalogMetrics | undefined;
23
+ }
24
+ /**
25
+ * Fill each model's `int`/`tps` from `index`. Returns the input array when no
26
+ * model changed so callers can keep identity-based caches.
27
+ */
28
+ export declare function applyCatalogMetrics<TApi extends Api>(models: Model<TApi>[], index: CatalogMetricsIndex): Model<TApi>[];
@@ -9,6 +9,13 @@ export declare function isZeroCostXaiOAuthReference(candidate: Model<Api>): bool
9
9
  * Pure: callers are responsible for memoizing the result.
10
10
  */
11
11
  export declare function buildModelReferenceIndex(models: Iterable<Model<Api>>): ModelReferenceIndex;
12
+ /**
13
+ * Expand a proxied/affixed model id into the ids it may be catalogued under,
14
+ * least-stripped first: bracket affixes, model-like segments, `:cloud`,
15
+ * namespace prefix, `:`→`-`, lowercase, and declared trailing markers.
16
+ * Shared by reference recovery and catalog-metric matching (`./metrics`).
17
+ */
18
+ export declare function getReferenceCandidateIds(modelId: string): string[];
12
19
  /**
13
20
  * Inherit bundled reference thinking only for same-provider matches. Wire routing
14
21
  * (`effortRouting`) is provider-specific; cross-provider inheritance can rewrite
@@ -31,6 +31,8 @@ export interface ModelsDevModel {
31
31
  provider?: {
32
32
  npm?: string;
33
33
  };
34
+ int?: number;
35
+ tps?: number;
34
36
  }
35
37
  /**
36
38
  * Fetches the models.dev catalog via catalog.stencil.so, which serves a
@@ -997,6 +997,10 @@ export interface Model<TApi extends Api = Api> {
997
997
  isRecommended?: boolean;
998
998
  /** Canonical thinking capability metadata for this model. */
999
999
  thinking?: ThinkingConfig;
1000
+ /** Intelligence score delivered by the model catalog. */
1001
+ int?: number | null;
1002
+ /** Catalog-estimated output speed in tokens per second. */
1003
+ tps?: number | null;
1000
1004
  /**
1001
1005
  * Fully-resolved compatibility record, materialized once by `buildModel`.
1002
1006
  * Protocol handlers read fields; they never detect, resolve, or allocate.
@@ -1031,6 +1035,12 @@ export interface Model<TApi extends Api = Api> {
1031
1035
  guardrailVersion?: string;
1032
1036
  /** Bedrock guardrail trace verbosity. */
1033
1037
  guardrailTrace?: "enabled" | "disabled" | "enabled_full";
1038
+ /**
1039
+ * Bedrock invocation-log tags attached to every Converse request for this
1040
+ * model. Set from `providers.<provider>.requestMetadata`; the Bedrock
1041
+ * transport reads it directly and validates it against AWS's limits.
1042
+ */
1043
+ requestMetadata?: Record<string, string>;
1034
1044
  }
1035
1045
  /**
1036
1046
  * A model as authored by configs, bundled catalogs, and discovery — the input
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-catalog",
4
- "version": "18.1.5",
4
+ "version": "18.1.6",
5
5
  "description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Stencil Labs, Inc.",
@@ -35,12 +35,12 @@
35
35
  "gen:proto": "bun scripts/generate-protocols.ts"
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/omptype": "18.1.5",
39
- "@oh-my-pi/pi-utils": "18.1.5"
38
+ "@oh-my-pi/omptype": "18.1.6",
39
+ "@oh-my-pi/pi-utils": "18.1.6"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@bgotink/kdl": "0.4.0",
43
- "@oh-my-pi/pi-ai": "18.1.5",
43
+ "@oh-my-pi/pi-ai": "18.1.6",
44
44
  "@types/bun": "^1.3.14"
45
45
  },
46
46
  "engines": {
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Catalog metric lookup (intelligence / speed scores) across provider id
3
+ * dialects. The shared catalog scores a model once under its vendor id
4
+ * (`claude-fable-5-1`); hosts re-spell it (`anthropic/claude-fable-5.1`,
5
+ * `global.anthropic.claude-fable-5-1`, `claude-fable-5-1-high`, `k3`), so an
6
+ * index keyed by exact id leaves most of the picker blank.
7
+ *
8
+ * Resolution order, first hit wins:
9
+ * 1. exact lowercased id;
10
+ * 2. candidate ids derived from the wire id (namespace and dotted vendor/region
11
+ * prefixes, bracket affixes, date/`vN:0` suffixes, declared markers, the
12
+ * taxonomy logical id) compared on a dot/colon-folded bare name, accepted
13
+ * only when the classified identities agree.
14
+ *
15
+ * Identity alone (class/family/revision) is deliberately not a key: it
16
+ * conflates `gpt-5.5` with `gpt-5.5-pro` and every Llama with the one that
17
+ * happens to be scored.
18
+ */
19
+ import type { ModelIdentity } from "../compat/types";
20
+ import type { Api, Model } from "../types";
21
+ import { bareModelId } from "./id";
22
+ import { getReferenceCandidateIds } from "./reference";
23
+
24
+ /** Catalog-delivered intelligence score and output speed for one model. */
25
+ export interface CatalogMetrics {
26
+ int?: number;
27
+ tps?: number;
28
+ }
29
+
30
+ interface ScoredEntry extends CatalogMetrics {
31
+ identity: ModelIdentity;
32
+ }
33
+
34
+ /** Trailing tokens Bedrock-style ids append without changing the model: `-v1:0`, `-v2`, `:0`, `-20251001`, `-2026-04-23`. */
35
+ const STRIPPABLE_SUFFIX_PATTERN = /(?:-v\d+(?::\d+)?|:\d+|-\d{2,})$/;
36
+ /** Leading alphabetic dotted namespaces: `global.anthropic.`, `us-gov.`, `openai.`. */
37
+ const DOTTED_PREFIX_PATTERN = /^[a-z][a-z-]*\./;
38
+
39
+ function canonicalKey(id: string): string {
40
+ return bareModelId(id).toLowerCase().replace(/[.:]/g, "-");
41
+ }
42
+
43
+ /** Whether two classified identities may describe the same scored model. Unset ranks on either side are not evidence. */
44
+ function identitiesAgree(left: ModelIdentity, right: ModelIdentity): boolean {
45
+ if (left.class !== right.class || left.class === "unknown") return false;
46
+ if (left.family !== undefined && right.family !== undefined && left.family !== right.family) return false;
47
+ if (left.revision !== undefined && right.revision !== undefined && left.revision !== right.revision) return false;
48
+ if (left.effort !== undefined && right.effort !== undefined && left.effort !== right.effort) return false;
49
+ return (left.thinkingVariant ?? false) === (right.thinkingVariant ?? false);
50
+ }
51
+
52
+ /** The catalog metrics a model carries, or undefined when it reports none. A zero speed is "unmeasured", not a score. */
53
+ export function catalogMetricsOf(model: Model<Api>): CatalogMetrics | undefined {
54
+ const int = model.int != null && Number.isFinite(model.int) ? model.int : undefined;
55
+ const tps = model.tps != null && Number.isFinite(model.tps) && model.tps > 0 ? model.tps : undefined;
56
+ if (int === undefined && tps === undefined) return undefined;
57
+ return { ...(int !== undefined ? { int } : {}), ...(tps !== undefined ? { tps } : {}) };
58
+ }
59
+
60
+ // Wire ids form a bounded set (bundled + discovered), so no eviction is needed.
61
+ const candidateCache = new Map<string, string[]>();
62
+
63
+ /** Candidate ids for `modelId`, least-stripped first, each already canonical-keyed. */
64
+ function metricCandidateKeys(modelId: string): string[] {
65
+ const cached = candidateCache.get(modelId);
66
+ if (cached) return cached;
67
+ const keys: string[] = [];
68
+ const seen = new Set<string>();
69
+ const queue = getReferenceCandidateIds(modelId);
70
+ for (let index = 0; index < queue.length; index++) {
71
+ const candidate = queue[index].toLowerCase();
72
+ const key = canonicalKey(candidate);
73
+ if (!seen.has(key)) {
74
+ seen.add(key);
75
+ keys.push(key);
76
+ }
77
+ const bare = bareModelId(candidate);
78
+ const withoutPrefix = bare.replace(DOTTED_PREFIX_PATTERN, "");
79
+ if (withoutPrefix !== bare && withoutPrefix.length > 0) queue.push(withoutPrefix);
80
+ const withoutSuffix = bare.replace(STRIPPABLE_SUFFIX_PATTERN, "");
81
+ if (withoutSuffix !== bare && withoutSuffix.length > 0) queue.push(withoutSuffix);
82
+ }
83
+ candidateCache.set(modelId, keys);
84
+ return keys;
85
+ }
86
+
87
+ /**
88
+ * Index of catalog metrics over every scored model seen so far. Built once per
89
+ * discovery cycle by the model registry and per provider by the model manager;
90
+ * `add` accumulates across providers so a proxy id resolves against any host's
91
+ * scored row.
92
+ */
93
+ export class CatalogMetricsIndex {
94
+ #exact = new Map<string, CatalogMetrics>();
95
+ #canonical = new Map<string, ScoredEntry>();
96
+
97
+ constructor(models?: Iterable<Model<Api>>) {
98
+ if (models) this.add(models);
99
+ }
100
+
101
+ get isEmpty(): boolean {
102
+ return this.#exact.size === 0;
103
+ }
104
+
105
+ /** Record the metrics of every scored model; later rows fill fields earlier rows left unset. */
106
+ add(models: Iterable<Model<Api>>): void {
107
+ for (const model of models) {
108
+ const metrics = catalogMetricsOf(model);
109
+ if (!metrics) continue;
110
+ const exactKey = model.id.toLowerCase();
111
+ const existing = this.#exact.get(exactKey);
112
+ this.#exact.set(exactKey, existing ? { ...metrics, ...existing } : metrics);
113
+
114
+ const canonical = canonicalKey(model.id);
115
+ const scored = this.#canonical.get(canonical);
116
+ if (!scored) this.#canonical.set(canonical, { ...metrics, identity: model.identity });
117
+ else if (scored.int === undefined || scored.tps === undefined) {
118
+ this.#canonical.set(canonical, { ...metrics, ...scored });
119
+ }
120
+ }
121
+ }
122
+
123
+ /** Metrics for `model` by exact id, else by dialect-normalized id when the classified identities agree. */
124
+ resolve(model: Model<Api>): CatalogMetrics | undefined {
125
+ const exact = this.#exact.get(model.id.toLowerCase());
126
+ if (exact) return exact;
127
+ const identity = model.identity;
128
+ const ids = identity.logicalId ? [model.id, identity.logicalId] : [model.id];
129
+ for (const id of ids) {
130
+ for (const key of metricCandidateKeys(id)) {
131
+ const scored = this.#canonical.get(key);
132
+ if (scored && identitiesAgree(identity, scored.identity)) return scored;
133
+ }
134
+ }
135
+ return undefined;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Fill each model's `int`/`tps` from `index`. Returns the input array when no
141
+ * model changed so callers can keep identity-based caches.
142
+ */
143
+ export function applyCatalogMetrics<TApi extends Api>(
144
+ models: Model<TApi>[],
145
+ index: CatalogMetricsIndex,
146
+ ): Model<TApi>[] {
147
+ if (index.isEmpty) return models;
148
+ let changed: Model<TApi>[] | undefined;
149
+ for (let position = 0; position < models.length; position++) {
150
+ const model = models[position];
151
+ if (model.int != null && model.tps != null) continue;
152
+ const metrics = index.resolve(model);
153
+ if (!metrics) continue;
154
+ const int = metrics.int ?? model.int;
155
+ const tps = metrics.tps ?? model.tps;
156
+ if (int === model.int && tps === model.tps) continue;
157
+ changed ??= [...models];
158
+ changed[position] = {
159
+ ...model,
160
+ ...(int != null ? { int } : {}),
161
+ ...(tps != null ? { tps } : {}),
162
+ };
163
+ }
164
+ return changed ?? models;
165
+ }
@@ -119,7 +119,13 @@ function stripReferenceTrailingMarker(candidate: string): string | undefined {
119
119
  return suffixLength > 0 && suffixLength < candidate.length ? candidate.slice(0, -suffixLength) : undefined;
120
120
  }
121
121
 
122
- function getReferenceCandidateIds(modelId: string): string[] {
122
+ /**
123
+ * Expand a proxied/affixed model id into the ids it may be catalogued under,
124
+ * least-stripped first: bracket affixes, model-like segments, `:cloud`,
125
+ * namespace prefix, `:`→`-`, lowercase, and declared trailing markers.
126
+ * Shared by reference recovery and catalog-metric matching (`./metrics`).
127
+ */
128
+ export function getReferenceCandidateIds(modelId: string): string[] {
123
129
  const candidates = new Set<string>();
124
130
  const queue = [modelId];
125
131
  for (let index = 0; index < queue.length; index += 1) {
@@ -1,5 +1,6 @@
1
1
  import { buildModel } from "./build";
2
2
  import { collapseBuiltVariants } from "./compat/collapse";
3
+ import { applyCatalogMetrics, CatalogMetricsIndex } from "./identity/metrics";
3
4
  import { readModelCache, writeModelCache } from "./model-cache";
4
5
  import { type GeneratedProvider, getBundledModels } from "./models";
5
6
  import type { Api, Model, ModelCost, ModelSpec, Provider, TokenCost } from "./types";
@@ -257,7 +258,7 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
257
258
  ? restoredCache.models.filter(model => !additiveStaticModelIds.has(model.id))
258
259
  : restoredCache.models;
259
260
  const cachedModels = additiveStaticModelIds
260
- ? mergeDynamicModels(staticModels, cacheContribution)
261
+ ? mergeCatalogMetrics(mergeDynamicModels(staticModels, cacheContribution), restoredCache.models)
261
262
  : restoredCache.models;
262
263
  const source: ModelResolutionSource = cacheContribution.length > 0 ? "cache" : "bundled";
263
264
  return {
@@ -312,7 +313,11 @@ export async function resolveProviderModels<TApi extends Api = Api, TModelsDevPa
312
313
  : modelsDevFetchSucceeded;
313
314
  const mergedWithCache = mergeDynamicModels(staticModels, cacheModels);
314
315
  const mergedWithModelsDev = mergeDynamicModels(mergedWithCache, modelsDevModels);
315
- const mergedModels = mergeDynamicModels(mergedWithModelsDev, dynamicModels);
316
+ const catalogMetricsSource = modelsDevFetchSucceeded ? normalizedModelsDevModels : preparedCacheModels;
317
+ const mergedWithCatalogMetrics = additiveStaticModelIds
318
+ ? mergeCatalogMetrics(mergedWithModelsDev, catalogMetricsSource)
319
+ : mergedWithModelsDev;
320
+ const mergedModels = mergeDynamicModels(mergedWithCatalogMetrics, dynamicModels);
316
321
  const models = collapseBuiltVariants(
317
322
  authoritativeDynamicFetchSucceeded ? retainModelIds(mergedModels, dynamicModels) : mergedModels,
318
323
  );
@@ -467,6 +472,14 @@ function prepareCacheModelsForStaticMismatch<TApi extends Api>(
467
472
  return sanitizedModels;
468
473
  }
469
474
 
475
+ function mergeCatalogMetrics<TApi extends Api>(
476
+ models: Model<TApi>[],
477
+ catalogModels: readonly Model<TApi>[],
478
+ ): Model<TApi>[] {
479
+ if (models.length === 0 || catalogModels.length === 0) return models;
480
+ return applyCatalogMetrics(models, new CatalogMetricsIndex(catalogModels));
481
+ }
482
+
470
483
  function mergeDynamicModels<TApi extends Api>(
471
484
  baseModels: readonly Model<TApi>[],
472
485
  dynamicModels: readonly Model<TApi>[],
@@ -110,6 +110,8 @@ export interface ModelsDevModel {
110
110
  };
111
111
  status?: string;
112
112
  provider?: { npm?: string };
113
+ int?: number;
114
+ tps?: number;
113
115
  }
114
116
 
115
117
  function toModelName(value: unknown, fallback: string): string {
@@ -6518,6 +6520,8 @@ export function mapModelsDevToModels(
6518
6520
  },
6519
6521
  contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? null),
6520
6522
  maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? null),
6523
+ ...(m.int != null ? { int: m.int } : {}),
6524
+ ...(m.tps != null ? { tps: m.tps } : {}),
6521
6525
  ...(m.tool_call === false ? { supportsTools: false } : {}),
6522
6526
  ...(desc.compat && { compat: desc.compat }),
6523
6527
  ...(desc.headers && { headers: { ...desc.headers } }),
package/src/types.ts CHANGED
@@ -1173,6 +1173,10 @@ export interface Model<TApi extends Api = Api> {
1173
1173
  isRecommended?: boolean;
1174
1174
  /** Canonical thinking capability metadata for this model. */
1175
1175
  thinking?: ThinkingConfig;
1176
+ /** Intelligence score delivered by the model catalog. */
1177
+ int?: number | null;
1178
+ /** Catalog-estimated output speed in tokens per second. */
1179
+ tps?: number | null;
1176
1180
  /**
1177
1181
  * Fully-resolved compatibility record, materialized once by `buildModel`.
1178
1182
  * Protocol handlers read fields; they never detect, resolve, or allocate.
@@ -1207,6 +1211,12 @@ export interface Model<TApi extends Api = Api> {
1207
1211
  guardrailVersion?: string;
1208
1212
  /** Bedrock guardrail trace verbosity. */
1209
1213
  guardrailTrace?: "enabled" | "disabled" | "enabled_full";
1214
+ /**
1215
+ * Bedrock invocation-log tags attached to every Converse request for this
1216
+ * model. Set from `providers.<provider>.requestMetadata`; the Bedrock
1217
+ * transport reads it directly and validates it against AWS's limits.
1218
+ */
1219
+ requestMetadata?: Record<string, string>;
1210
1220
  }
1211
1221
 
1212
1222
  /**