@oh-my-pi/pi-catalog 17.4.0 → 17.4.2

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.
@@ -32,7 +32,7 @@
32
32
  * generator, and the model-manager merge point.
33
33
  */
34
34
  import { buildCompat, buildModel } from "./build";
35
- import { Effort } from "./effort";
35
+ import { Effort, THINKING_EFFORTS } from "./effort";
36
36
  import { stripThinkingVariantToken } from "./identity/family";
37
37
  import { resolveModelThinking } from "./model-thinking";
38
38
  import type { Api, Model, ModelSpec, Provider, ThinkingConfig } from "./types";
@@ -711,6 +711,8 @@ export const DEVIN_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
711
711
  /** Cursor's Grok tier tokens equal the effort id, so routing is a direct map. */
712
712
  const CURSOR_GROK_45_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High];
713
713
  const CURSOR_GROK_46_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh];
714
+ /** Cursor GPT-5.6 (Luna/Sol/Terra) serves the full five-tier `low..max` scale. */
715
+ const CURSOR_GPT_56_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Effort.High, Effort.XHigh, Effort.Max];
714
716
 
715
717
  /**
716
718
  * Cursor serves Grok 4.5/4.6 as per-effort sibling ids
@@ -731,14 +733,189 @@ function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]):
731
733
  return [build(false), build(true)];
732
734
  }
733
735
 
734
- /** `cursor` Grok families: per-effort siblings collapsed per service-tier lane. */
736
+ /**
737
+ * Cursor serves GPT-5.6 (Luna/Sol/Terra) as per-tier sibling ids
738
+ * (`gpt-5.6-luna-none|-low|-medium|-high|-xhigh|-max`) with a parallel `-fast`
739
+ * service-tier lane (`gpt-5.6-luna-high-fast`, …). Same shape as Devin's
740
+ * `devinGpt56Families`, but cursor keys the version with a dot, marks the
741
+ * thinking-off tier `-none`, and names the fast lane with `-fast` (Devin uses
742
+ * `-priority`). Each lane collapses into its own logical model — `-fast` is a
743
+ * sibling SKU, never a second routing dimension — while the 1M / Max Mode SKU
744
+ * stays a separate row (handled by discovery's context-window resolution).
745
+ */
746
+ function cursorGpt56Families(variant: "luna" | "sol" | "terra", name: string): readonly EffortVariantFamily[] {
747
+ const build = (fast: boolean): EffortVariantFamily => {
748
+ const suffix = fast ? "-fast" : "";
749
+ const base = `gpt-5.6-${variant}`;
750
+ return tierFamily(
751
+ `${base}${suffix}`,
752
+ `${name}${fast ? " Fast" : ""}`,
753
+ {
754
+ off: `${base}-none${suffix}`,
755
+ low: `${base}-low${suffix}`,
756
+ medium: `${base}-medium${suffix}`,
757
+ high: `${base}-high${suffix}`,
758
+ xhigh: `${base}-xhigh${suffix}`,
759
+ max: `${base}-max${suffix}`,
760
+ },
761
+ CURSOR_GPT_56_EFFORTS,
762
+ );
763
+ };
764
+ return [build(false), build(true)];
765
+ }
766
+
767
+ /** `cursor` per-effort sibling families collapsed per service-tier lane: Grok 4.5/4.6 plus GPT-5.6 Luna/Sol/Terra. */
735
768
  export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
736
769
  families: [
737
770
  ...cursorGrokFamilies("4.5", CURSOR_GROK_45_EFFORTS),
738
771
  ...cursorGrokFamilies("4.6", CURSOR_GROK_46_EFFORTS),
772
+ ...cursorGpt56Families("luna", "GPT-5.6 Luna"),
773
+ ...cursorGpt56Families("sol", "GPT-5.6 Sol"),
774
+ ...cursorGpt56Families("terra", "GPT-5.6 Terra"),
739
775
  ],
740
776
  };
741
777
 
778
+ type CursorTierToken = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
779
+
780
+ interface CursorTierMember<TSpec extends VariantSpecLike> {
781
+ baseId: string;
782
+ fast: boolean;
783
+ spec: TSpec;
784
+ tier: CursorTierToken;
785
+ }
786
+
787
+ const CURSOR_TIER_ID_PATTERN = /^(.+?)-(extra-high|none|minimal|low|medium|high|xhigh|max)(-fast)?$/;
788
+ const CURSOR_TIER_BASE_PATTERN = /-(extra-high|none|minimal|low|medium|high|xhigh|max)$/;
789
+ const CURSOR_THINKING_TOKEN_PATTERN = /(^|-)thinking($|-)/;
790
+ const CURSOR_TIER_BY_TOKEN: Readonly<Record<string, CursorTierToken | undefined>> = {
791
+ none: "none",
792
+ minimal: "minimal",
793
+ low: "low",
794
+ medium: "medium",
795
+ high: "high",
796
+ "extra-high": "xhigh",
797
+ xhigh: "xhigh",
798
+ max: "max",
799
+ };
800
+
801
+ /** Whether an existing logical row already routes every live member in `members`. */
802
+ function collapsedCursorLogicalMatches<TSpec extends VariantSpecLike>(
803
+ spec: TSpec,
804
+ members: readonly CursorTierMember<TSpec>[],
805
+ ): boolean {
806
+ const routing = spec.thinking?.effortRouting;
807
+ if (!routing) return false;
808
+ for (const member of members) {
809
+ if (spec.requestModelId === member.spec.id) continue;
810
+ let matched = false;
811
+ for (const effort of VARIANT_ROUTING_KEYS) {
812
+ if (routing[effort] === member.spec.id) {
813
+ matched = true;
814
+ break;
815
+ }
816
+ }
817
+ if (!matched) return false;
818
+ }
819
+ return true;
820
+ }
821
+
822
+ /**
823
+ * Derive safe Cursor per-effort families from live wire ids. A family is
824
+ * intentionally left expanded when its base is an independent live SKU, any
825
+ * member already has a thinking ladder, member metadata differs, or a tier
826
+ * token is also part of a product name.
827
+ */
828
+ function deriveCursorEffortFamilies<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): EffortVariantFamily[] {
829
+ const byId = new Map<string, TSpec>();
830
+ const groups = new Map<string, CursorTierMember<TSpec>[]>();
831
+ const candidateBases = new Set<string>();
832
+
833
+ for (const spec of specs) {
834
+ if (!byId.has(spec.id)) byId.set(spec.id, spec);
835
+ const match = CURSOR_TIER_ID_PATTERN.exec(spec.id);
836
+ if (!match) continue;
837
+ const baseId = match[1];
838
+ const tier = CURSOR_TIER_BY_TOKEN[match[2] ?? ""];
839
+ const fast = match[3] !== undefined;
840
+ if (!baseId || !tier) continue;
841
+ const member = { baseId, fast, spec, tier };
842
+ const key = `${baseId}\0${fast ? "fast" : "standard"}`;
843
+ const group = groups.get(key);
844
+ if (group) {
845
+ group.push(member);
846
+ } else {
847
+ groups.set(key, [member]);
848
+ }
849
+ candidateBases.add(baseId);
850
+ }
851
+
852
+ const unsafeBases = new Set<string>();
853
+ for (const group of groups.values()) {
854
+ const first = group[0];
855
+ if (!first) continue;
856
+ const { baseId } = first;
857
+ const standardGroup = groups.get(`${baseId}\0standard`) ?? [];
858
+ const standardBase = byId.get(baseId);
859
+ const laneBase = byId.get(`${baseId}${first.fast ? "-fast" : ""}`);
860
+ const independentStandardBase =
861
+ standardBase !== undefined && !collapsedCursorLogicalMatches(standardBase, standardGroup);
862
+ const independentLaneBase = laneBase !== undefined && !collapsedCursorLogicalMatches(laneBase, group);
863
+ if (
864
+ independentStandardBase ||
865
+ independentLaneBase ||
866
+ new Set(group.map(member => member.tier)).size !== group.length ||
867
+ CURSOR_TIER_BASE_PATTERN.test(baseId) ||
868
+ CURSOR_THINKING_TOKEN_PATTERN.test(baseId) ||
869
+ candidateBases.has(`${baseId}-thinking`) ||
870
+ byId.has(`${baseId}-thinking`) ||
871
+ byId.has(`${baseId}-thinking-fast`) ||
872
+ byId.has(`${baseId}-fast-thinking`) ||
873
+ group.some(member => member.spec.thinking !== undefined || member.spec.requestModelId !== undefined) ||
874
+ group.some(member => candidateBases.has(`${baseId}-${member.tier}`))
875
+ ) {
876
+ unsafeBases.add(baseId);
877
+ }
878
+ }
879
+
880
+ const families: EffortVariantFamily[] = [];
881
+ for (const group of groups.values()) {
882
+ const first = group[0];
883
+ if (!first || group.length < 2 || unsafeBases.has(first.baseId)) continue;
884
+ if (
885
+ group.some(
886
+ member =>
887
+ member.spec.api !== first.spec.api ||
888
+ member.spec.baseUrl !== first.spec.baseUrl ||
889
+ member.spec.contextWindow !== first.spec.contextWindow ||
890
+ member.spec.maxTokens !== first.spec.maxTokens ||
891
+ member.spec.cursorMaxMode !== first.spec.cursorMaxMode ||
892
+ !Bun.deepEquals(member.spec.cost, first.spec.cost) ||
893
+ !Bun.deepEquals(member.spec.compat, first.spec.compat),
894
+ )
895
+ ) {
896
+ continue;
897
+ }
898
+
899
+ const routes: TierRoutes = {};
900
+ for (const member of group) {
901
+ if (member.tier === "none") {
902
+ routes.off = member.spec.id;
903
+ } else {
904
+ routes[member.tier] = member.spec.id;
905
+ }
906
+ }
907
+ const efforts = THINKING_EFFORTS.filter(effort => routes[effort] !== undefined);
908
+ if (efforts.length === 0) continue;
909
+ const strippedName = first.spec.name
910
+ .replace(/\s+(extra-high|none|minimal|low|medium|high|xhigh|max)(\s+fast)?$/i, "")
911
+ .trim();
912
+ const baseName = strippedName === first.spec.id ? first.baseId : strippedName || first.baseId;
913
+ const suffix = first.fast ? "-fast" : "";
914
+ families.push(tierFamily(`${first.baseId}${suffix}`, `${baseName}${first.fast ? " Fast" : ""}`, routes, efforts));
915
+ }
916
+ return families;
917
+ }
918
+
742
919
  /** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
743
920
  export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = {
744
921
  "google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
@@ -1124,10 +1301,59 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
1124
1301
  }
1125
1302
 
1126
1303
  /**
1127
- * Collapse a full mixed-provider list: per provider, the hand table (when
1128
- * registered) plus the automatic `X`/`X-thinking` pair rule. Used by the
1129
- * catalog generator; the runtime equivalent lives at the model-manager merge
1130
- * point. Output is regrouped by provider — callers re-sort.
1304
+ * Re-key model-to-model configuration after collapse removes a referenced
1305
+ * member id. Qualified targets keep their provider; bare targets remain bare.
1306
+ */
1307
+ function retargetCollapsedModelReferences<TSpec extends VariantSpecLike>(specs: TSpec[]): void {
1308
+ const liveIdsByProvider = new Map<string, Set<string>>();
1309
+ for (const spec of specs) {
1310
+ const provider = spec.provider.toLowerCase();
1311
+ let liveIds = liveIdsByProvider.get(provider);
1312
+ if (!liveIds) {
1313
+ liveIds = new Set<string>();
1314
+ liveIdsByProvider.set(provider, liveIds);
1315
+ }
1316
+ liveIds.add(spec.id.toLowerCase());
1317
+ }
1318
+
1319
+ for (let index = 0; index < specs.length; index++) {
1320
+ const spec = specs[index];
1321
+ if (!spec) continue;
1322
+ const contextPromotionTarget = resolveCollapsedModelReference(
1323
+ spec.contextPromotionTarget,
1324
+ spec.provider,
1325
+ liveIdsByProvider,
1326
+ );
1327
+ const compactionModel = resolveCollapsedModelReference(spec.compactionModel, spec.provider, liveIdsByProvider);
1328
+ if (contextPromotionTarget === spec.contextPromotionTarget && compactionModel === spec.compactionModel) continue;
1329
+ specs[index] = { ...spec, contextPromotionTarget, compactionModel };
1330
+ }
1331
+ }
1332
+
1333
+ function resolveCollapsedModelReference(
1334
+ target: string | undefined,
1335
+ currentProvider: Provider,
1336
+ liveIdsByProvider: ReadonlyMap<string, ReadonlySet<string>>,
1337
+ ): string | undefined {
1338
+ if (target === undefined) return undefined;
1339
+ const separator = target.indexOf("/");
1340
+ const provider = separator >= 0 ? target.slice(0, separator) : currentProvider;
1341
+ const providerId = provider.toLowerCase();
1342
+ const modelId = separator >= 0 ? target.slice(separator + 1) : target;
1343
+ const normalizedModelId = modelId.trim().toLowerCase();
1344
+ const liveIds = liveIdsByProvider.get(providerId);
1345
+ if (liveIds?.has(normalizedModelId)) return target;
1346
+ const alias = resolveRegisteredVariantAlias(provider, normalizedModelId);
1347
+ if (alias === undefined || !liveIds?.has(alias.toLowerCase())) return target;
1348
+ return separator >= 0 ? `${provider}/${alias}` : alias;
1349
+ }
1350
+
1351
+ /**
1352
+ * Collapse a full mixed-provider list: per provider, the hand table, Cursor's
1353
+ * conservative live effort-sibling rule, and the automatic `X`/`X-thinking`
1354
+ * pair rule. Used by the catalog generator; the runtime equivalent lives at
1355
+ * the model-manager merge point. Output is regrouped by provider — callers
1356
+ * re-sort.
1131
1357
  */
1132
1358
  export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): TSpec[] {
1133
1359
  const byProvider = new Map<string, TSpec[]>();
@@ -1143,12 +1369,20 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
1143
1369
  for (const [provider, slice] of byProvider) {
1144
1370
  const table = VARIANT_COLLAPSE_TABLES[provider];
1145
1371
  let result = table ? collapseEffortVariants(slice, table) : slice;
1372
+ if (provider === "cursor") {
1373
+ const cursorDerived = deriveCursorEffortFamilies(result);
1374
+ if (cursorDerived.length > 0) {
1375
+ result = collapseEffortVariants(result, { families: cursorDerived });
1376
+ }
1377
+ }
1146
1378
  const derived = deriveThinkingPairFamilies(result, table);
1147
1379
  if (derived.length > 0) {
1148
1380
  result = collapseEffortVariants(result, { families: derived });
1149
1381
  }
1382
+ registerCollapsedVariantAliases(provider, result);
1150
1383
  out.push(...result);
1151
1384
  }
1385
+ retargetCollapsedModelReferences(out);
1152
1386
  return out;
1153
1387
  }
1154
1388
 
@@ -1172,83 +1406,125 @@ interface VariantAliasIndex {
1172
1406
  /** lowercased retired id → replacement model id. */
1173
1407
  forward: Map<string, string>;
1174
1408
  /** replacement model id → retired ids that resolve to it. */
1175
- reverse: Map<string, readonly string[]>;
1176
- /** Collapsed logical ids declared by the table. */
1409
+ reverse: Map<string, string[]>;
1410
+ /** Collapsed logical ids declared by the table or observed at runtime. */
1177
1411
  familyIds: Set<string>;
1178
1412
  }
1179
1413
 
1414
+ const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
1415
+ const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
1416
+
1180
1417
  const kAliasIndex = Symbol("variant-collapse.aliasIndex");
1181
1418
 
1182
1419
  interface TableWithAliasIndex extends VariantCollapseTable {
1183
1420
  [kAliasIndex]?: VariantAliasIndex;
1184
1421
  }
1185
1422
 
1423
+ function createAliasIndex(): VariantAliasIndex {
1424
+ return {
1425
+ forward: new Map<string, string>(),
1426
+ reverse: new Map<string, string[]>(),
1427
+ familyIds: new Set<string>(),
1428
+ };
1429
+ }
1430
+
1431
+ function addVariantAlias(index: VariantAliasIndex, from: string, to: string): boolean {
1432
+ if (from === to || index.forward.has(from.toLowerCase())) return false;
1433
+ index.forward.set(from.toLowerCase(), to);
1434
+ const sources = index.reverse.get(to);
1435
+ if (sources) {
1436
+ sources.push(from);
1437
+ } else {
1438
+ index.reverse.set(to, [from]);
1439
+ }
1440
+ return true;
1441
+ }
1442
+
1443
+ /**
1444
+ * Persist aliases embedded in collapsed routing so generated catalog rows and
1445
+ * newly discovered families expose the same selector migrations as hand tables.
1446
+ */
1447
+ function registerCollapsedVariantAliases(provider: Provider, specs: readonly VariantSpecLike[]): void {
1448
+ const providerId = provider.toLowerCase();
1449
+ let index = dynamicAliasIndexes.get(providerId);
1450
+ for (const spec of specs) {
1451
+ const routing = spec.thinking?.effortRouting;
1452
+ if (!routing) continue;
1453
+ let registered = false;
1454
+ for (const effort of VARIANT_ROUTING_KEYS) {
1455
+ const source = routing[effort];
1456
+ if (!source || source === spec.id) continue;
1457
+ index ??= createAliasIndex();
1458
+ registered = addVariantAlias(index, source, spec.id) || registered;
1459
+ }
1460
+ if (spec.requestModelId && spec.requestModelId !== spec.id) {
1461
+ index ??= createAliasIndex();
1462
+ registered = addVariantAlias(index, spec.requestModelId, spec.id) || registered;
1463
+ }
1464
+ if (registered) index?.familyIds.add(spec.id);
1465
+ }
1466
+ if (index) dynamicAliasIndexes.set(providerId, index);
1467
+ }
1468
+
1469
+ function resolveRegisteredVariantAlias(provider: Provider, normalizedModelId: string): string | undefined {
1470
+ const providerId = provider.toLowerCase();
1471
+ const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
1472
+ return (
1473
+ (table ? getAliasIndex(table).forward.get(normalizedModelId) : undefined) ??
1474
+ dynamicAliasIndexes.get(providerId)?.forward.get(normalizedModelId)
1475
+ );
1476
+ }
1477
+
1186
1478
  function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
1187
1479
  const tagged = table as TableWithAliasIndex;
1188
1480
  const cached = tagged[kAliasIndex];
1189
1481
  if (cached) return cached;
1190
- const forward = new Map<string, string>();
1191
- const reverse = new Map<string, string[]>();
1192
- const add = (from: string, to: string) => {
1193
- if (from === to) return;
1194
- forward.set(from.toLowerCase(), to);
1195
- const sources = reverse.get(to);
1196
- if (sources) {
1197
- sources.push(from);
1198
- } else {
1199
- reverse.set(to, [from]);
1200
- }
1201
- };
1202
- const familyIds = new Set<string>();
1482
+ const index = createAliasIndex();
1203
1483
  for (const family of table.families) {
1204
- familyIds.add(family.id);
1205
- for (const member of family.members) add(member, family.id);
1206
- for (const alias of family.extraAliases ?? []) add(alias, family.id);
1484
+ index.familyIds.add(family.id);
1485
+ for (const member of family.members) addVariantAlias(index, member, family.id);
1486
+ for (const alias of family.extraAliases ?? []) addVariantAlias(index, alias, family.id);
1207
1487
  }
1208
- const index: VariantAliasIndex = { forward, reverse, familyIds };
1209
1488
  tagged[kAliasIndex] = index;
1210
1489
  return index;
1211
1490
  }
1212
1491
 
1213
1492
  /**
1214
1493
  * Resolve a retired effort-tier variant id (collapsed member, recycled id) to
1215
- * its replacement model id for `provider` via the hand table. Returns
1216
- * `undefined` when the id is not a known alias; derived `X-thinking` members
1217
- * resolve through `stripThinkingVariantToken` instead. Callers must try an
1218
- * exact model lookup first — a live model always wins over an alias.
1494
+ * its replacement model id for `provider` via hand-table or registered live
1495
+ * aliases. Returns `undefined` when the id is not a known alias; derived
1496
+ * `X-thinking` members also resolve through `stripThinkingVariantToken`.
1497
+ * Callers must try an exact model lookup first — a live model always wins over
1498
+ * an alias.
1219
1499
  */
1220
1500
  export function resolveVariantAlias(provider: Provider, modelId: string): string | undefined {
1221
- const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[provider.toLowerCase()];
1222
- if (!table) return undefined;
1223
- return getAliasIndex(table).forward.get(modelId.trim().toLowerCase());
1501
+ return resolveRegisteredVariantAlias(provider, modelId.trim().toLowerCase());
1224
1502
  }
1225
1503
 
1226
1504
  /** Bare-id alias hit: replacement id plus the providers declaring it. */
1227
1505
  export interface BareVariantAliasHit {
1228
1506
  id: string;
1229
- /** Providers whose table declares the alias — candidates from these win ties. */
1507
+ /** Providers declaring the alias — candidates from these win ties. */
1230
1508
  providers: readonly Provider[];
1231
1509
  }
1232
1510
 
1233
1511
  /**
1234
- * Provider-agnostic hand-table alias lookup for bare-id selectors. Returns
1235
- * the declaring providers so callers can prefer their models when the
1236
- * replacement id exists on unrelated providers too (e.g. a retired Cursor
1237
- * tier id must not resolve to `openai/gpt-5.4`).
1512
+ * Provider-agnostic alias lookup for bare-id selectors. Returns the declaring
1513
+ * providers so callers can prefer their models when the replacement id exists
1514
+ * on unrelated providers too (e.g. a retired Cursor tier id must not resolve
1515
+ * to `openai/gpt-5.4`).
1238
1516
  */
1239
1517
  export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit | undefined {
1240
1518
  const normalized = modelId.trim().toLowerCase();
1241
- for (const provider in VARIANT_COLLAPSE_TABLES) {
1242
- const table = VARIANT_COLLAPSE_TABLES[provider] as VariantCollapseTable;
1243
- const hit = getAliasIndex(table).forward.get(normalized);
1519
+ const providerIds = new Set<string>();
1520
+ for (const provider in VARIANT_COLLAPSE_TABLES) providerIds.add(provider);
1521
+ for (const provider of dynamicAliasIndexes.keys()) providerIds.add(provider);
1522
+ for (const provider of providerIds) {
1523
+ const hit = resolveRegisteredVariantAlias(provider, normalized);
1244
1524
  if (hit === undefined) continue;
1245
1525
  const providers: Provider[] = [];
1246
- for (const candidate in VARIANT_COLLAPSE_TABLES) {
1247
- // Match by resolved alias target, not table identity: the CCA providers
1248
- // now hold distinct table objects that still share these aliases.
1249
- if (
1250
- getAliasIndex(VARIANT_COLLAPSE_TABLES[candidate] as VariantCollapseTable).forward.get(normalized) === hit
1251
- ) {
1526
+ for (const candidate of providerIds) {
1527
+ if (resolveRegisteredVariantAlias(candidate, normalized) === hit) {
1252
1528
  providers.push(candidate);
1253
1529
  }
1254
1530
  }
@@ -1259,14 +1535,18 @@ export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit |
1259
1535
 
1260
1536
  /**
1261
1537
  * Reverse alias lookup: the retired ids that resolve to `modelId` for
1262
- * `provider` via the hand table. Used to re-key config keyed by raw member
1263
- * ids (models.yml `modelOverrides`, suppressed selectors) onto the collapsed
1264
- * model. Empty for providers without a table.
1538
+ * `provider` via hand-table or registered live aliases. Used to re-key config
1539
+ * keyed by raw member ids (models.yml `modelOverrides`, suppressed selectors)
1540
+ * onto the collapsed model.
1265
1541
  */
1266
1542
  export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
1267
- const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[provider.toLowerCase()];
1268
- if (!table) return [];
1269
- return getAliasIndex(table).reverse.get(modelId) ?? [];
1543
+ const providerId = provider.toLowerCase();
1544
+ const table = VARIANT_COLLAPSE_TABLES[provider] ?? VARIANT_COLLAPSE_TABLES[providerId];
1545
+ const staticSources = table ? getAliasIndex(table).reverse.get(modelId) : undefined;
1546
+ const dynamicSources = dynamicAliasIndexes.get(providerId)?.reverse.get(modelId);
1547
+ if (!staticSources) return dynamicSources ?? [];
1548
+ if (!dynamicSources) return staticSources;
1549
+ return [...new Set([...staticSources, ...dynamicSources])];
1270
1550
  }
1271
1551
 
1272
1552
  function maxOrNull(values: ReadonlyArray<number | null>): number | null {
package/src/wire/codex.ts CHANGED
@@ -29,6 +29,8 @@ export const OPENAI_HEADERS = {
29
29
  RESPONSES_LITE: "x-openai-internal-codex-responses-lite",
30
30
  /** DeviceCheck attestation envelope (codex-rs `X_OAI_ATTESTATION_HEADER`); sent on ChatGPT-OAuth requests. */
31
31
  ATTESTATION: "x-oai-attestation",
32
+ /** Client-declared data residency for region-pinned enterprise workspaces. */
33
+ RESIDENCY: "x-openai-internal-codex-residency",
32
34
  } as const;
33
35
 
34
36
  export const OPENAI_HEADER_VALUES = {
@@ -61,3 +63,55 @@ export function getCodexAccountId(accessToken: string): string | undefined {
61
63
  return undefined;
62
64
  }
63
65
  }
66
+
67
+ /**
68
+ * Extract the account's data residency from a Codex JWT access token.
69
+ *
70
+ * Enterprise ChatGPT workspaces can be pinned to a region. Such a workspace
71
+ * rejects a Codex request whose egress does not match it — HTTP 401
72
+ * `Workspace is not authorized in this region.` — unless the client declares
73
+ * the residency itself. The token already carries it, so no configuration is
74
+ * needed: `chatgpt_data_residency` is the authoritative claim, with
75
+ * `chatgpt_compute_residency` as the fallback for tokens that only carry that.
76
+ *
77
+ * Returns undefined for a non-JWT token, or for the (common) accounts whose
78
+ * claims omit residency entirely — those workspaces are not region-pinned.
79
+ */
80
+ export function getCodexResidency(accessToken: string): string | undefined {
81
+ try {
82
+ const parts = accessToken.split(".");
83
+ if (parts.length !== 3) return undefined;
84
+ const decoded = Buffer.from(parts[1] ?? "", "base64").toString("utf-8");
85
+ const payload = JSON.parse(decoded) as Record<string, unknown>;
86
+ const auth = payload[JWT_CLAIM_PATH] as
87
+ | { chatgpt_data_residency?: unknown; chatgpt_compute_residency?: unknown }
88
+ | undefined;
89
+ for (const claim of [auth?.chatgpt_data_residency, auth?.chatgpt_compute_residency]) {
90
+ if (typeof claim !== "string") continue;
91
+ const residency = claim.trim();
92
+ if (residency.length > 0) return residency;
93
+ }
94
+ return undefined;
95
+ } catch {
96
+ return undefined;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Adds the token's workspace residency to Codex request headers without
102
+ * replacing a caller-supplied value.
103
+ */
104
+ export function applyCodexResidencyHeader(headers: Headers | Record<string, string>, accessToken: string): void {
105
+ const headerName = OPENAI_HEADERS.RESIDENCY;
106
+ if (headers instanceof Headers) {
107
+ if (headers.has(headerName)) return;
108
+ const residency = getCodexResidency(accessToken);
109
+ if (residency) headers.set(headerName, residency);
110
+ return;
111
+ }
112
+ for (const configuredName in headers) {
113
+ if (configuredName.toLowerCase() === headerName) return;
114
+ }
115
+ const residency = getCodexResidency(accessToken);
116
+ if (residency) headers[headerName] = residency;
117
+ }