@oh-my-pi/pi-catalog 17.4.1 → 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 +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/cache-provider-id.d.ts +2 -0
- package/dist/types/variant-collapse.d.ts +18 -16
- package/dist/types/wire/image-fetchers.d.ts +76 -0
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/models.json +328 -1383
- package/src/provider-models/cache-provider-id.ts +11 -0
- package/src/variant-collapse.ts +295 -51
- package/src/wire/image-fetchers.ts +159 -0
|
@@ -5,6 +5,17 @@ export interface ModelCacheProviderIdOptions {
|
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS: Readonly<Record<string, true>> = {
|
|
9
|
+
"opencode-go": true,
|
|
10
|
+
"opencode-zen": true,
|
|
11
|
+
"github-copilot": true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Whether a provider's model-cache namespace requires its resolved credential. */
|
|
15
|
+
export function isCredentialScopedModelCacheProvider(providerId: string): boolean {
|
|
16
|
+
return CREDENTIAL_SCOPED_MODEL_CACHE_PROVIDERS[providerId] === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
export function getDefaultModelDiscoveryBaseUrl(providerId: string): string | undefined {
|
|
9
20
|
switch (providerId) {
|
|
10
21
|
case "ollama":
|
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";
|
|
@@ -775,6 +775,147 @@ export const CURSOR_VARIANT_COLLAPSE_TABLE: VariantCollapseTable = {
|
|
|
775
775
|
],
|
|
776
776
|
};
|
|
777
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
|
+
|
|
778
919
|
/** Provider id → hand collapse table. The CCA providers diverge on thinking transport. */
|
|
779
920
|
export const VARIANT_COLLAPSE_TABLES: Readonly<Record<string, VariantCollapseTable>> = {
|
|
780
921
|
"google-antigravity": ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
|
|
@@ -1160,10 +1301,59 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1160
1301
|
}
|
|
1161
1302
|
|
|
1162
1303
|
/**
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1165
|
-
|
|
1166
|
-
|
|
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.
|
|
1167
1357
|
*/
|
|
1168
1358
|
export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecLike>(specs: readonly TSpec[]): TSpec[] {
|
|
1169
1359
|
const byProvider = new Map<string, TSpec[]>();
|
|
@@ -1179,12 +1369,20 @@ export function collapseEffortVariantsAcrossProviders<TSpec extends VariantSpecL
|
|
|
1179
1369
|
for (const [provider, slice] of byProvider) {
|
|
1180
1370
|
const table = VARIANT_COLLAPSE_TABLES[provider];
|
|
1181
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
|
+
}
|
|
1182
1378
|
const derived = deriveThinkingPairFamilies(result, table);
|
|
1183
1379
|
if (derived.length > 0) {
|
|
1184
1380
|
result = collapseEffortVariants(result, { families: derived });
|
|
1185
1381
|
}
|
|
1382
|
+
registerCollapsedVariantAliases(provider, result);
|
|
1186
1383
|
out.push(...result);
|
|
1187
1384
|
}
|
|
1385
|
+
retargetCollapsedModelReferences(out);
|
|
1188
1386
|
return out;
|
|
1189
1387
|
}
|
|
1190
1388
|
|
|
@@ -1208,83 +1406,125 @@ interface VariantAliasIndex {
|
|
|
1208
1406
|
/** lowercased retired id → replacement model id. */
|
|
1209
1407
|
forward: Map<string, string>;
|
|
1210
1408
|
/** replacement model id → retired ids that resolve to it. */
|
|
1211
|
-
reverse: Map<string,
|
|
1212
|
-
/** Collapsed logical ids declared by the table. */
|
|
1409
|
+
reverse: Map<string, string[]>;
|
|
1410
|
+
/** Collapsed logical ids declared by the table or observed at runtime. */
|
|
1213
1411
|
familyIds: Set<string>;
|
|
1214
1412
|
}
|
|
1215
1413
|
|
|
1414
|
+
const dynamicAliasIndexes = new Map<string, VariantAliasIndex>();
|
|
1415
|
+
const VARIANT_ROUTING_KEYS: readonly (Effort | "off")[] = ["off", ...THINKING_EFFORTS];
|
|
1416
|
+
|
|
1216
1417
|
const kAliasIndex = Symbol("variant-collapse.aliasIndex");
|
|
1217
1418
|
|
|
1218
1419
|
interface TableWithAliasIndex extends VariantCollapseTable {
|
|
1219
1420
|
[kAliasIndex]?: VariantAliasIndex;
|
|
1220
1421
|
}
|
|
1221
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
|
+
|
|
1222
1478
|
function getAliasIndex(table: VariantCollapseTable): VariantAliasIndex {
|
|
1223
1479
|
const tagged = table as TableWithAliasIndex;
|
|
1224
1480
|
const cached = tagged[kAliasIndex];
|
|
1225
1481
|
if (cached) return cached;
|
|
1226
|
-
const
|
|
1227
|
-
const reverse = new Map<string, string[]>();
|
|
1228
|
-
const add = (from: string, to: string) => {
|
|
1229
|
-
if (from === to) return;
|
|
1230
|
-
forward.set(from.toLowerCase(), to);
|
|
1231
|
-
const sources = reverse.get(to);
|
|
1232
|
-
if (sources) {
|
|
1233
|
-
sources.push(from);
|
|
1234
|
-
} else {
|
|
1235
|
-
reverse.set(to, [from]);
|
|
1236
|
-
}
|
|
1237
|
-
};
|
|
1238
|
-
const familyIds = new Set<string>();
|
|
1482
|
+
const index = createAliasIndex();
|
|
1239
1483
|
for (const family of table.families) {
|
|
1240
|
-
familyIds.add(family.id);
|
|
1241
|
-
for (const member of family.members)
|
|
1242
|
-
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);
|
|
1243
1487
|
}
|
|
1244
|
-
const index: VariantAliasIndex = { forward, reverse, familyIds };
|
|
1245
1488
|
tagged[kAliasIndex] = index;
|
|
1246
1489
|
return index;
|
|
1247
1490
|
}
|
|
1248
1491
|
|
|
1249
1492
|
/**
|
|
1250
1493
|
* Resolve a retired effort-tier variant id (collapsed member, recycled id) to
|
|
1251
|
-
* its replacement model id for `provider` via
|
|
1252
|
-
* `undefined` when the id is not a known alias; derived
|
|
1253
|
-
*
|
|
1254
|
-
* 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.
|
|
1255
1499
|
*/
|
|
1256
1500
|
export function resolveVariantAlias(provider: Provider, modelId: string): string | undefined {
|
|
1257
|
-
|
|
1258
|
-
if (!table) return undefined;
|
|
1259
|
-
return getAliasIndex(table).forward.get(modelId.trim().toLowerCase());
|
|
1501
|
+
return resolveRegisteredVariantAlias(provider, modelId.trim().toLowerCase());
|
|
1260
1502
|
}
|
|
1261
1503
|
|
|
1262
1504
|
/** Bare-id alias hit: replacement id plus the providers declaring it. */
|
|
1263
1505
|
export interface BareVariantAliasHit {
|
|
1264
1506
|
id: string;
|
|
1265
|
-
/** Providers
|
|
1507
|
+
/** Providers declaring the alias — candidates from these win ties. */
|
|
1266
1508
|
providers: readonly Provider[];
|
|
1267
1509
|
}
|
|
1268
1510
|
|
|
1269
1511
|
/**
|
|
1270
|
-
* Provider-agnostic
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
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`).
|
|
1274
1516
|
*/
|
|
1275
1517
|
export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit | undefined {
|
|
1276
1518
|
const normalized = modelId.trim().toLowerCase();
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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);
|
|
1280
1524
|
if (hit === undefined) continue;
|
|
1281
1525
|
const providers: Provider[] = [];
|
|
1282
|
-
for (const candidate
|
|
1283
|
-
|
|
1284
|
-
// now hold distinct table objects that still share these aliases.
|
|
1285
|
-
if (
|
|
1286
|
-
getAliasIndex(VARIANT_COLLAPSE_TABLES[candidate] as VariantCollapseTable).forward.get(normalized) === hit
|
|
1287
|
-
) {
|
|
1526
|
+
for (const candidate of providerIds) {
|
|
1527
|
+
if (resolveRegisteredVariantAlias(candidate, normalized) === hit) {
|
|
1288
1528
|
providers.push(candidate);
|
|
1289
1529
|
}
|
|
1290
1530
|
}
|
|
@@ -1295,14 +1535,18 @@ export function resolveBareVariantAlias(modelId: string): BareVariantAliasHit |
|
|
|
1295
1535
|
|
|
1296
1536
|
/**
|
|
1297
1537
|
* Reverse alias lookup: the retired ids that resolve to `modelId` for
|
|
1298
|
-
* `provider` via
|
|
1299
|
-
* ids (models.yml `modelOverrides`, suppressed selectors)
|
|
1300
|
-
*
|
|
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.
|
|
1301
1541
|
*/
|
|
1302
1542
|
export function getVariantAliasSources(provider: Provider, modelId: string): readonly string[] {
|
|
1303
|
-
const
|
|
1304
|
-
|
|
1305
|
-
|
|
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])];
|
|
1306
1550
|
}
|
|
1307
1551
|
|
|
1308
1552
|
function maxOrNull(values: ReadonlyArray<number | null>): number | null {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identification of provider-side image fetchers.
|
|
3
|
+
*
|
|
4
|
+
* A request may carry an image either inline (base64) or as a URL. With a URL,
|
|
5
|
+
* the provider's backend performs its own server-side GET against that URL, so
|
|
6
|
+
* a local blob server sees an inbound request from vendor infrastructure rather
|
|
7
|
+
* than from the client. This module names those fetchers, letting a blob server
|
|
8
|
+
* attribute an inbound GET to the vendor that issued it — which is how a caller
|
|
9
|
+
* learns where a request actually landed when a router sits between them and
|
|
10
|
+
* the model.
|
|
11
|
+
*
|
|
12
|
+
* NOT an authentication mechanism. Every value here is a request header chosen
|
|
13
|
+
* by the caller and is trivially forged. Authorize blob reads with an
|
|
14
|
+
* unguessable URL (single-use capability token, short TTL) and treat a fetcher
|
|
15
|
+
* match as attribution/telemetry only.
|
|
16
|
+
*
|
|
17
|
+
* Each entry was captured from a live fetch triggered by handing that vendor's
|
|
18
|
+
* API a URL-sourced image.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Vendor whose infrastructure performed an inbound fetch. */
|
|
22
|
+
export type ImageFetcherVendor = "openai" | "anthropic" | "xai" | "google";
|
|
23
|
+
|
|
24
|
+
/** Registry key for a known fetcher. */
|
|
25
|
+
export type ImageFetcherId =
|
|
26
|
+
| "openai-file-downloader"
|
|
27
|
+
| "anthropic-claude-user"
|
|
28
|
+
| "anthropic-claude-user-preview"
|
|
29
|
+
| "xai-image-api-fetch"
|
|
30
|
+
| "google";
|
|
31
|
+
|
|
32
|
+
/** Request signature of one provider-side fetcher. */
|
|
33
|
+
export interface ImageFetcherIdentity {
|
|
34
|
+
vendor: ImageFetcherVendor;
|
|
35
|
+
/** Human-readable name for logs and UI. */
|
|
36
|
+
label: string;
|
|
37
|
+
/**
|
|
38
|
+
* `User-Agent` contract: an exact string for fetchers that send a fixed
|
|
39
|
+
* value, or a pattern for those embedding a client version.
|
|
40
|
+
*/
|
|
41
|
+
userAgent: string | RegExp;
|
|
42
|
+
/**
|
|
43
|
+
* Vendor-proprietary headers observed alongside the agent, used to
|
|
44
|
+
* corroborate a `User-Agent` claim. Generic infrastructure headers
|
|
45
|
+
* (`traceparent`, `x-cloud-trace-context`) are deliberately excluded: they
|
|
46
|
+
* are emitted by unrelated infrastructure and corroborate nothing. Empty
|
|
47
|
+
* means the agent string is the only available signal, so
|
|
48
|
+
* {@link ImageFetcherMatch.corroborated} can never be true for that entry.
|
|
49
|
+
*/
|
|
50
|
+
markerHeaders: readonly string[];
|
|
51
|
+
/** API surface the capture came from. */
|
|
52
|
+
observedVia: string;
|
|
53
|
+
/** Operational caveats a blob server should account for. */
|
|
54
|
+
note?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Known provider-side fetchers.
|
|
59
|
+
*
|
|
60
|
+
* Agent contracts do not overlap — exact strings never collide with the
|
|
61
|
+
* versioned patterns — so lookup order carries no meaning.
|
|
62
|
+
*/
|
|
63
|
+
export const IMAGE_FETCHERS: Readonly<Record<ImageFetcherId, ImageFetcherIdentity>> = {
|
|
64
|
+
"openai-file-downloader": {
|
|
65
|
+
vendor: "openai",
|
|
66
|
+
label: "OpenAI File Downloader",
|
|
67
|
+
userAgent: "OpenAI File Downloader",
|
|
68
|
+
markerHeaders: ["openai-internal-smokescreener"],
|
|
69
|
+
observedVia: "chatgpt.com/backend-api/codex/responses, input_image.image_url",
|
|
70
|
+
note: "Issues two near-simultaneous GETs per image; a blob server must treat a duplicate hit as expected rather than as replay.",
|
|
71
|
+
},
|
|
72
|
+
"anthropic-claude-user": {
|
|
73
|
+
vendor: "anthropic",
|
|
74
|
+
label: "Claude image fetcher",
|
|
75
|
+
userAgent: "Claude-User",
|
|
76
|
+
markerHeaders: [],
|
|
77
|
+
observedVia: "api.anthropic.com/v1/messages, image.source.type=url",
|
|
78
|
+
note: "Sends only generic trace headers, so the bare agent string is the sole signal. Distinct from the versioned Claude-User/<version> agent used for links appearing in conversation text.",
|
|
79
|
+
},
|
|
80
|
+
"anthropic-claude-user-preview": {
|
|
81
|
+
vendor: "anthropic",
|
|
82
|
+
label: "Claude link fetcher",
|
|
83
|
+
userAgent: /\bClaude-User\/\d+(?:\.\d+)*\b/,
|
|
84
|
+
markerHeaders: [],
|
|
85
|
+
observedVia:
|
|
86
|
+
"unsolicited fetch after a URL appeared in assistant output; never observed serving an image request",
|
|
87
|
+
note: "Not an image fetcher. Listed so a blob server can separate it from the image path instead of counting it as a provider image fetch.",
|
|
88
|
+
},
|
|
89
|
+
"xai-image-api-fetch": {
|
|
90
|
+
vendor: "xai",
|
|
91
|
+
label: "xAI image API fetch",
|
|
92
|
+
userAgent: /^XaiImageApiFetch\/\d+(?:\.\d+)*\s/,
|
|
93
|
+
markerHeaders: ["x-xaifetchid"],
|
|
94
|
+
observedVia: "api.x.ai/v1/responses, input_image.image_url",
|
|
95
|
+
note: "Sends an image-only `accept` allowlist and rejects any other content type before the model sees the response.",
|
|
96
|
+
},
|
|
97
|
+
google: {
|
|
98
|
+
vendor: "google",
|
|
99
|
+
label: "Google",
|
|
100
|
+
userAgent: "Google",
|
|
101
|
+
markerHeaders: [],
|
|
102
|
+
observedVia: "cloudcode-pa.googleapis.com v1internal:streamGenerateContent, fileData.fileUri",
|
|
103
|
+
note: "Weakest signal of the set: the agent string is a bare vendor name with no version and no proprietary header.",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Attribution outcome for one inbound request. */
|
|
108
|
+
export interface ImageFetcherMatch {
|
|
109
|
+
id: ImageFetcherId;
|
|
110
|
+
identity: ImageFetcherIdentity;
|
|
111
|
+
/**
|
|
112
|
+
* Whether every proprietary marker header for the matched identity is
|
|
113
|
+
* present. Always false for identities declaring no markers — absence of
|
|
114
|
+
* corroboration is not evidence against the match.
|
|
115
|
+
*/
|
|
116
|
+
corroborated: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Inbound header bag, as exposed by either `fetch` or a Node-style server. */
|
|
120
|
+
export type InboundHeaders = Headers | Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
121
|
+
|
|
122
|
+
function headerValue(headers: InboundHeaders, name: string): string | undefined {
|
|
123
|
+
if (headers instanceof Headers) return headers.get(name) ?? undefined;
|
|
124
|
+
const direct = headers[name];
|
|
125
|
+
if (direct !== undefined) return Array.isArray(direct) ? direct[0] : (direct as string);
|
|
126
|
+
for (const key in headers) {
|
|
127
|
+
if (key.toLowerCase() !== name) continue;
|
|
128
|
+
const value = headers[key];
|
|
129
|
+
return Array.isArray(value) ? value[0] : (value as string | undefined);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Attribute an inbound blob request to a known provider-side fetcher, or
|
|
136
|
+
* `null` when the agent matches none.
|
|
137
|
+
*
|
|
138
|
+
* Matches on `User-Agent` alone and reports marker-header corroboration
|
|
139
|
+
* separately; callers MUST NOT treat either as proof of origin. Gate access on
|
|
140
|
+
* the unguessable URL.
|
|
141
|
+
*/
|
|
142
|
+
export function identifyImageFetcher(headers: InboundHeaders): ImageFetcherMatch | null {
|
|
143
|
+
const agent = headerValue(headers, "user-agent");
|
|
144
|
+
if (!agent) return null;
|
|
145
|
+
for (const id in IMAGE_FETCHERS) {
|
|
146
|
+
const identity = IMAGE_FETCHERS[id as ImageFetcherId];
|
|
147
|
+
const { userAgent } = identity;
|
|
148
|
+
const matched = typeof userAgent === "string" ? agent === userAgent : userAgent.test(agent);
|
|
149
|
+
if (!matched) continue;
|
|
150
|
+
return {
|
|
151
|
+
id: id as ImageFetcherId,
|
|
152
|
+
identity,
|
|
153
|
+
corroborated:
|
|
154
|
+
identity.markerHeaders.length > 0 &&
|
|
155
|
+
identity.markerHeaders.every(header => headerValue(headers, header) !== undefined),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|