@oh-my-pi/pi-catalog 18.1.5 → 18.1.8
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 +20 -0
- package/dist/types/discovery/codex.d.ts +7 -0
- package/dist/types/identity/metrics.d.ts +28 -0
- package/dist/types/identity/reference.d.ts +7 -0
- package/dist/types/provider-models/openai-compat.d.ts +2 -0
- package/dist/types/types.d.ts +17 -0
- package/dist/types/wire/codex.d.ts +20 -2
- package/package.json +4 -4
- package/src/compat/axes.ts +1 -0
- package/src/compat/resolve.ts +2 -0
- package/src/compat/rules/auth/openai-codex.kdl +1 -1
- package/src/compat/rules/classes/openai.kdl +6 -0
- package/src/compat/rules/providers/openai-codex.kdl +2 -2
- package/src/compat/rules/providers/openai.kdl +1 -1
- package/src/compat/rules.json +16 -3
- package/src/discovery/codex.ts +10 -0
- package/src/identity/metrics.ts +165 -0
- package/src/identity/reference.ts +7 -1
- package/src/model-manager.ts +15 -2
- package/src/models.json +4861 -382
- package/src/provider-models/openai-compat.ts +4 -0
- package/src/provider-models/special.ts +25 -10
- package/src/types.ts +17 -0
- package/src/wire/codex.ts +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.8] - 2026-09-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added GPT-6 Astra to the OpenAI Codex model catalog, including support for configuration updates and requests using the freeform `apply_patch` tool.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed `omp models refresh` so revoked ChatGPT account tokens no longer prevent the remaining OpenAI Codex models from being discovered.
|
|
14
|
+
|
|
15
|
+
## [18.1.6] - 2026-09-03
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added catalog-delivered model intelligence scores and estimated output throughput to help compare model capabilities and performance.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Improved model search and selection so configured roles, provider preferences, and recent usage are prioritized while browsing and filtering models.
|
|
24
|
+
|
|
5
25
|
## [18.1.5] - 2026-09-03
|
|
6
26
|
|
|
7
27
|
### Added
|
|
@@ -26,6 +26,13 @@ export interface CodexModelDiscoveryOptions {
|
|
|
26
26
|
export interface CodexModelDiscoveryResult {
|
|
27
27
|
models: ModelSpec<"openai-codex-responses">[];
|
|
28
28
|
etag?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Set when the backend rejected the credential itself (401/403, e.g.
|
|
31
|
+
* `token_revoked`); `models` is empty. A definitive per-account denial,
|
|
32
|
+
* unlike the `null` result for transport/parse failures, so multi-account
|
|
33
|
+
* discovery can skip the account instead of aborting.
|
|
34
|
+
*/
|
|
35
|
+
rejectedStatus?: 401 | 403;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Fetches model metadata from Codex backend and normalizes it for pi model management.
|
|
@@ -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
|
package/dist/types/types.d.ts
CHANGED
|
@@ -700,6 +700,13 @@ export interface ResolvedOpenAIResponsesCompat extends ResolvedOpenAISharedCompa
|
|
|
700
700
|
* transport; earlier ids reject the value.
|
|
701
701
|
*/
|
|
702
702
|
supportsAllTurnsReasoningContext: boolean;
|
|
703
|
+
/**
|
|
704
|
+
* Whether a `configuration_update` input item may change `reasoning.effort`
|
|
705
|
+
* mid-conversation while the request-level effort stays byte-stable for
|
|
706
|
+
* prompt caching. Rule-owned: GPT-6 Astra only; every other model rejects
|
|
707
|
+
* the item type with 400.
|
|
708
|
+
*/
|
|
709
|
+
supportsConfigurationUpdate: boolean;
|
|
703
710
|
/** Inject the `# Juice: 0 !important` developer item when reasoning is forced off (gpt-5.6+). */
|
|
704
711
|
requiresReasoningOffJuiceInstruction: boolean;
|
|
705
712
|
/**
|
|
@@ -997,6 +1004,10 @@ export interface Model<TApi extends Api = Api> {
|
|
|
997
1004
|
isRecommended?: boolean;
|
|
998
1005
|
/** Canonical thinking capability metadata for this model. */
|
|
999
1006
|
thinking?: ThinkingConfig;
|
|
1007
|
+
/** Intelligence score delivered by the model catalog. */
|
|
1008
|
+
int?: number | null;
|
|
1009
|
+
/** Catalog-estimated output speed in tokens per second. */
|
|
1010
|
+
tps?: number | null;
|
|
1000
1011
|
/**
|
|
1001
1012
|
* Fully-resolved compatibility record, materialized once by `buildModel`.
|
|
1002
1013
|
* Protocol handlers read fields; they never detect, resolve, or allocate.
|
|
@@ -1031,6 +1042,12 @@ export interface Model<TApi extends Api = Api> {
|
|
|
1031
1042
|
guardrailVersion?: string;
|
|
1032
1043
|
/** Bedrock guardrail trace verbosity. */
|
|
1033
1044
|
guardrailTrace?: "enabled" | "disabled" | "enabled_full";
|
|
1045
|
+
/**
|
|
1046
|
+
* Bedrock invocation-log tags attached to every Converse request for this
|
|
1047
|
+
* model. Set from `providers.<provider>.requestMetadata`; the Bedrock
|
|
1048
|
+
* transport reads it directly and validates it against AWS's limits.
|
|
1049
|
+
*/
|
|
1050
|
+
requestMetadata?: Record<string, string>;
|
|
1034
1051
|
}
|
|
1035
1052
|
/**
|
|
1036
1053
|
* A model as authored by configs, bundled catalogs, and discovery — the input
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
5
5
|
/**
|
|
6
6
|
* Pinned OpenAI Codex client version (corresponds to @openai/codex package version).
|
|
7
|
+
*
|
|
8
|
+
* The backend version-gates model availability against this value on both
|
|
9
|
+
* `/models?client_version=` and `/responses` (`gpt-6-astra` requires ≥ 0.153.0);
|
|
10
|
+
* an older pin silently hides newer SKUs from discovery.
|
|
7
11
|
*/
|
|
8
|
-
export declare const CODEX_CLIENT_VERSION = "0.
|
|
12
|
+
export declare const CODEX_CLIENT_VERSION = "0.153.0";
|
|
9
13
|
export declare const OPENAI_HEADERS: {
|
|
10
14
|
readonly BETA: "OpenAI-Beta";
|
|
11
15
|
/** Codex feature-negotiation header; values identify opt-in wire protocols. */
|
|
@@ -28,18 +32,32 @@ export declare const OPENAI_HEADERS: {
|
|
|
28
32
|
readonly ATTESTATION: "x-oai-attestation";
|
|
29
33
|
/** Client-declared data residency for region-pinned enterprise workspaces. */
|
|
30
34
|
readonly RESIDENCY: "x-openai-internal-codex-residency";
|
|
35
|
+
/**
|
|
36
|
+
* Model routing hint (codex-rs `X_CODEX_ROUTING_HINT_HEADER`): `model=<slug>`
|
|
37
|
+
* or `model=<slug>;tier=<service_tier>`; sent on every ChatGPT-OAuth
|
|
38
|
+
* Responses, compaction, and WebSocket handshake request. Built by
|
|
39
|
+
* {@link codexRoutingHint}.
|
|
40
|
+
*/
|
|
41
|
+
readonly ROUTING_HINT: "x-codex-routing-hint";
|
|
31
42
|
};
|
|
32
43
|
export declare const OPENAI_HEADER_VALUES: {
|
|
33
44
|
readonly BETA_RESPONSES: "responses=experimental";
|
|
34
45
|
readonly BETA_RESPONSES_WEBSOCKETS_V2: "responses_websockets=2026-02-06";
|
|
35
46
|
readonly REMOTE_COMPACTION_V2: "remote_compaction_v2";
|
|
36
|
-
readonly ORIGINATOR_CODEX: "
|
|
47
|
+
readonly ORIGINATOR_CODEX: "omp";
|
|
37
48
|
};
|
|
38
49
|
export declare const URL_PATHS: {
|
|
39
50
|
readonly RESPONSES: "/responses";
|
|
40
51
|
readonly CODEX_RESPONSES: "/codex/responses";
|
|
41
52
|
};
|
|
42
53
|
export declare const JWT_CLAIM_PATH: "https://api.openai.com/auth";
|
|
54
|
+
/**
|
|
55
|
+
* Build the `x-codex-routing-hint` value for a request (codex-rs
|
|
56
|
+
* `build_routing_hint_header`): the requested model slug plus the explicit
|
|
57
|
+
* service tier when one is set. Callers set it only on ChatGPT-OAuth requests
|
|
58
|
+
* to the Codex backend; API-key OpenAI traffic never carries it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function codexRoutingHint(model: string, serviceTier: string | null | undefined): string;
|
|
43
61
|
/**
|
|
44
62
|
* Extract account ID from a Codex JWT access token.
|
|
45
63
|
* Returns undefined if the token is not a valid Codex JWT.
|
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.
|
|
4
|
+
"version": "18.1.8",
|
|
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.
|
|
39
|
-
"@oh-my-pi/pi-utils": "18.1.
|
|
38
|
+
"@oh-my-pi/omptype": "18.1.8",
|
|
39
|
+
"@oh-my-pi/pi-utils": "18.1.8"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@bgotink/kdl": "0.4.0",
|
|
43
|
-
"@oh-my-pi/pi-ai": "18.1.
|
|
43
|
+
"@oh-my-pi/pi-ai": "18.1.8",
|
|
44
44
|
"@types/bun": "^1.3.14"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
package/src/compat/axes.ts
CHANGED
|
@@ -128,6 +128,7 @@ export const AXES: Readonly<Record<string, AxisDef>> = {
|
|
|
128
128
|
"strict-responses-pairing": wire("strictResponsesPairing", ["openai-responses"]),
|
|
129
129
|
"requires-reasoning-off-juice-instruction": wire("requiresReasoningOffJuiceInstruction", ["openai-responses"]),
|
|
130
130
|
"supports-all-turns-reasoning-context": wire("supportsAllTurnsReasoningContext", ["openai-responses"]),
|
|
131
|
+
"supports-configuration-update": wire("supportsConfigurationUpdate", ["openai-responses"]),
|
|
131
132
|
"strip-deepseek-special-tokens": wire("stripDeepseekSpecialTokens", OAI),
|
|
132
133
|
"stream-markup-healing-pattern": wire("streamMarkupHealingPattern", OAI, "scalar", [
|
|
133
134
|
"kimi",
|
package/src/compat/resolve.ts
CHANGED
|
@@ -714,6 +714,7 @@ function resolveOpenAIResponsesPolicy(
|
|
|
714
714
|
supportsImageDetailOriginal: !isXaiHost && !modelMatchesHost(hostModel, "githubCopilot"),
|
|
715
715
|
supportsReasoningSummary: !isXaiHost,
|
|
716
716
|
supportsAllTurnsReasoningContext: false,
|
|
717
|
+
supportsConfigurationUpdate: false,
|
|
717
718
|
requiresReasoningOffJuiceInstruction: false,
|
|
718
719
|
stripImageInput: false,
|
|
719
720
|
thinkingLoopGuard: undefined,
|
|
@@ -815,6 +816,7 @@ function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnly
|
|
|
815
816
|
supportsImageDetailOriginal: compat.supportsImageDetailOriginal,
|
|
816
817
|
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
|
817
818
|
supportsAllTurnsReasoningContext: compat.supportsAllTurnsReasoningContext,
|
|
819
|
+
supportsConfigurationUpdate: compat.supportsConfigurationUpdate,
|
|
818
820
|
officialEndpoint: compat.officialEndpoint,
|
|
819
821
|
harmonyLeakMitigation: compat.harmonyLeakMitigation,
|
|
820
822
|
cacheControlFormat: compat.cacheControlFormat,
|
|
@@ -8,7 +8,7 @@ auth "openai-codex" {
|
|
|
8
8
|
authorize-params {
|
|
9
9
|
id_token_add_organizations "true"
|
|
10
10
|
codex_cli_simplified_flow "true"
|
|
11
|
-
originator "
|
|
11
|
+
originator "omp"
|
|
12
12
|
}
|
|
13
13
|
instructions "A browser window should open. Complete login to finish."
|
|
14
14
|
// OpenAI only allowlists this exact URI; a busy port must fail rather than fall back.
|
|
@@ -30,6 +30,12 @@ class "openai" {
|
|
|
30
30
|
thinking-efforts "low" "medium" "high" "xhigh" "max"
|
|
31
31
|
requires-reasoning-off-juice-instruction #true
|
|
32
32
|
}
|
|
33
|
+
// GPT-6 Astra changes reasoning effort mid-conversation through a
|
|
34
|
+
// `configuration_update` input item so the request-level effort (and the
|
|
35
|
+
// cached prompt prefix) stays put; every other SKU 400s on the item type.
|
|
36
|
+
models "gpt-6-astra" {
|
|
37
|
+
supports-configuration-update #true
|
|
38
|
+
}
|
|
33
39
|
// Codex SKUs: the advertised 400K figure includes the output budget; the
|
|
34
40
|
// true prompt window is 272K. Spark budgets are its own family's.
|
|
35
41
|
family "codex" {
|
|
@@ -69,9 +69,9 @@ provider "openai-codex" {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
// First-party GPT-5 Responses SKUs support the freeform apply_patch
|
|
72
|
+
// First-party GPT-5/GPT-6 Responses SKUs support the freeform apply_patch
|
|
73
73
|
// custom tool with a Lark grammar.
|
|
74
|
-
revision ">=5 <
|
|
74
|
+
revision ">=5 <7" {
|
|
75
75
|
apply-patch-tool-type "freeform"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/compat/rules.json
CHANGED
|
@@ -6226,6 +6226,19 @@
|
|
|
6226
6226
|
{
|
|
6227
6227
|
"source": "classes/openai.kdl:36",
|
|
6228
6228
|
"class": "openai",
|
|
6229
|
+
"models": [
|
|
6230
|
+
{
|
|
6231
|
+
"kind": "exact",
|
|
6232
|
+
"value": "gpt-6-astra"
|
|
6233
|
+
}
|
|
6234
|
+
],
|
|
6235
|
+
"wire": {
|
|
6236
|
+
"supportsConfigurationUpdate": true
|
|
6237
|
+
}
|
|
6238
|
+
},
|
|
6239
|
+
{
|
|
6240
|
+
"source": "classes/openai.kdl:42",
|
|
6241
|
+
"class": "openai",
|
|
6229
6242
|
"family": "codex",
|
|
6230
6243
|
"revision": [
|
|
6231
6244
|
{
|
|
@@ -10650,7 +10663,7 @@
|
|
|
10650
10663
|
},
|
|
10651
10664
|
{
|
|
10652
10665
|
"op": "<",
|
|
10653
|
-
"revision": "
|
|
10666
|
+
"revision": "7.0.0"
|
|
10654
10667
|
}
|
|
10655
10668
|
],
|
|
10656
10669
|
"catalog": {
|
|
@@ -10955,7 +10968,7 @@
|
|
|
10955
10968
|
},
|
|
10956
10969
|
{
|
|
10957
10970
|
"op": "<",
|
|
10958
|
-
"revision": "
|
|
10971
|
+
"revision": "7.0.0"
|
|
10959
10972
|
}
|
|
10960
10973
|
],
|
|
10961
10974
|
"catalog": {
|
|
@@ -14919,7 +14932,7 @@
|
|
|
14919
14932
|
"authorizeParams": {
|
|
14920
14933
|
"id_token_add_organizations": "true",
|
|
14921
14934
|
"codex_cli_simplified_flow": "true",
|
|
14922
|
-
"originator": "
|
|
14935
|
+
"originator": "omp"
|
|
14923
14936
|
},
|
|
14924
14937
|
"clientId": {
|
|
14925
14938
|
"value": "app_EMoamEEZ73f0CkXaXp7hrann"
|
package/src/discovery/codex.ts
CHANGED
|
@@ -110,6 +110,13 @@ export interface CodexModelDiscoveryOptions {
|
|
|
110
110
|
export interface CodexModelDiscoveryResult {
|
|
111
111
|
models: ModelSpec<"openai-codex-responses">[];
|
|
112
112
|
etag?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Set when the backend rejected the credential itself (401/403, e.g.
|
|
115
|
+
* `token_revoked`); `models` is empty. A definitive per-account denial,
|
|
116
|
+
* unlike the `null` result for transport/parse failures, so multi-account
|
|
117
|
+
* discovery can skip the account instead of aborting.
|
|
118
|
+
*/
|
|
119
|
+
rejectedStatus?: 401 | 403;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
/**
|
|
@@ -139,6 +146,9 @@ export async function fetchCodexModels(options: CodexModelDiscoveryOptions): Pro
|
|
|
139
146
|
continue;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
if (response.status === 401 || response.status === 403) {
|
|
150
|
+
return { models: [], rejectedStatus: response.status };
|
|
151
|
+
}
|
|
142
152
|
if (!response.ok) {
|
|
143
153
|
continue;
|
|
144
154
|
}
|
|
@@ -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
|
-
|
|
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) {
|
package/src/model-manager.ts
CHANGED
|
@@ -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
|
|
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>[],
|