@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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +22 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/discovery/cursor-proto.d.ts +6 -0
- package/dist/types/identity/family.d.ts +7 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model-manager.d.ts +5 -5
- package/dist/types/provider-models/cache-provider-id.d.ts +2 -0
- package/dist/types/provider-models/descriptors.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +3 -1
- package/dist/types/types.d.ts +3 -1
- package/dist/types/variant-collapse.d.ts +19 -17
- package/dist/types/wire/codex.d.ts +21 -0
- package/dist/types/wire/image-fetchers.d.ts +76 -0
- package/package.json +7 -5
- package/src/compat/openai.ts +1 -1
- package/src/discovery/codex.ts +4 -0
- package/src/discovery/cursor-proto.ts +7 -0
- package/src/identity/family.ts +10 -0
- package/src/index.ts +1 -0
- package/src/model-cache.ts +3 -3
- package/src/model-manager.ts +5 -5
- package/src/models.json +599 -1651
- package/src/provider-models/cache-provider-id.ts +15 -2
- package/src/provider-models/google.ts +8 -6
- package/src/provider-models/openai-compat.ts +151 -48
- package/src/types.ts +3 -1
- package/src/variant-collapse.ts +332 -52
- package/src/wire/codex.ts +54 -0
- package/src/wire/image-fetchers.ts +159 -0
package/src/variant-collapse.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
|
|
1130
|
-
|
|
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,
|
|
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
|
|
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)
|
|
1206
|
-
for (const alias of family.extraAliases ?? [])
|
|
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
|
|
1216
|
-
* `undefined` when the id is not a known alias; derived
|
|
1217
|
-
*
|
|
1218
|
-
* exact model lookup first — a live model always wins over
|
|
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
|
-
|
|
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
|
|
1507
|
+
/** Providers declaring the alias — candidates from these win ties. */
|
|
1230
1508
|
providers: readonly Provider[];
|
|
1231
1509
|
}
|
|
1232
1510
|
|
|
1233
1511
|
/**
|
|
1234
|
-
* Provider-agnostic
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
|
1247
|
-
|
|
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
|
|
1263
|
-
* ids (models.yml `modelOverrides`, suppressed selectors)
|
|
1264
|
-
*
|
|
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
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
+
}
|