@oh-my-pi/pi-catalog 18.0.3 → 18.0.4
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 +8 -0
- package/dist/types/model-thinking.d.ts +11 -0
- package/dist/types/models.d.ts +5 -2
- package/dist/types/provider-models/openai-compat.d.ts +5 -0
- package/dist/types/types.d.ts +6 -3
- package/dist/types/variant-collapse.d.ts +9 -0
- package/package.json +4 -4
- package/src/model-thinking.ts +33 -7
- package/src/models.json +689 -761
- package/src/models.ts +15 -6
- package/src/provider-models/cache-provider-id.ts +4 -3
- package/src/provider-models/openai-compat.ts +62 -0
- package/src/types.ts +6 -3
- package/src/variant-collapse.ts +75 -11
package/src/models.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import MODELS from "./models.json" with { type: "json" };
|
|
2
|
-
import type { Api, KnownProvider, Model, TokenCost, Usage } from "./types";
|
|
2
|
+
import type { Api, KnownProvider, Model, ModelCost, TokenCost, Usage } from "./types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Static bundled model registry loaded from `models.json`.
|
|
@@ -43,23 +43,27 @@ export function getBundledModels(provider: GeneratedProvider): Model<Api>[] {
|
|
|
43
43
|
const models = getProviderModels(provider);
|
|
44
44
|
return models ? (Array.from(models.values()) as Model<Api>[]) : [];
|
|
45
45
|
}
|
|
46
|
-
function resolveTokenCost(cost:
|
|
46
|
+
function resolveTokenCost(cost: ModelCost, promptInputTokens: number): TokenCost {
|
|
47
47
|
const longContext = cost.longContext;
|
|
48
48
|
if (!longContext) return cost;
|
|
49
|
-
|
|
49
|
+
const reachesThreshold =
|
|
50
|
+
promptInputTokens > longContext.inputThreshold ||
|
|
51
|
+
(longContext.inputThresholdInclusive === true && promptInputTokens === longContext.inputThreshold);
|
|
52
|
+
return reachesThreshold ? longContext : cost;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/** Price a prompt as fully uncached input under its active context-length tier. */
|
|
53
|
-
export function calculateUncachedInputCost(cost:
|
|
56
|
+
export function calculateUncachedInputCost(cost: ModelCost, promptInputTokens: number): number {
|
|
54
57
|
const rates = resolveTokenCost(cost, promptInputTokens);
|
|
55
58
|
return (rates.input / 1_000_000) * promptInputTokens;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
/** Price one usage record from a token rate card, including active context tiers. */
|
|
62
|
+
export function calculateUsageCost(cost: ModelCost, usage: Usage): Usage["cost"] {
|
|
59
63
|
const orchestration = usage.orchestration;
|
|
60
64
|
const promptInputTokens =
|
|
61
65
|
usage.input + usage.cacheRead + usage.cacheWrite + (orchestration?.input ?? 0) + (orchestration?.cacheRead ?? 0);
|
|
62
|
-
const rates = resolveTokenCost(
|
|
66
|
+
const rates = resolveTokenCost(cost, promptInputTokens);
|
|
63
67
|
usage.cost.input = (rates.input / 1000000) * (usage.input + (orchestration?.input ?? 0));
|
|
64
68
|
usage.cost.output = (rates.output / 1000000) * (usage.output + (orchestration?.output ?? 0));
|
|
65
69
|
usage.cost.cacheRead = (rates.cacheRead / 1000000) * (usage.cacheRead + (orchestration?.cacheRead ?? 0));
|
|
@@ -68,6 +72,11 @@ export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage
|
|
|
68
72
|
return usage.cost;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
/** Price one usage record from its model's token rate card. */
|
|
76
|
+
export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
|
|
77
|
+
return calculateUsageCost(model.cost, usage);
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
/**
|
|
72
81
|
* Price cache-write tokens, honoring the TTL breakdown when the provider reports one.
|
|
73
82
|
*
|
|
@@ -54,9 +54,10 @@ export function resolveModelCacheProviderId(providerId: string, options: ModelCa
|
|
|
54
54
|
case "ollama":
|
|
55
55
|
return resolveOllamaModelCacheProviderId(providerId, options.baseUrl);
|
|
56
56
|
case "cursor":
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
57
|
+
// v4: Grok 4.5/4.6 rows cached before the effort-less default-tier fix
|
|
58
|
+
// carry `requestModelId: *-low`, which the Start plan refuses; refetch
|
|
59
|
+
// so the collapsed default is re-pointed to `-medium` (issue #9478).
|
|
60
|
+
return "cursor:default-effort-v4";
|
|
60
61
|
case "litellm": {
|
|
61
62
|
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
|
|
62
63
|
return `litellm:rich-v6:${Bun.hash(baseUrl).toString(36)}`;
|
|
@@ -1315,6 +1315,68 @@ export interface XaiModelManagerConfig {
|
|
|
1315
1315
|
fetch?: FetchImpl;
|
|
1316
1316
|
}
|
|
1317
1317
|
|
|
1318
|
+
// xAI bills the whole request at 2x the base rates once the prompt reaches
|
|
1319
|
+
// 200K tokens, for every current Grok SKU that carries the two-tier card
|
|
1320
|
+
// (grok-4.3/4.5/4.6, every grok-4.20 variant, grok-build-0.1). Source:
|
|
1321
|
+
// docs.x.ai/developers/models. Computing the tier from the base keeps it in
|
|
1322
|
+
// lockstep with stencil.so price updates instead of a hand-maintained table.
|
|
1323
|
+
const XAI_LONG_CONTEXT_THRESHOLD = 200_000;
|
|
1324
|
+
const XAI_LONG_CONTEXT_MULTIPLIER = 2;
|
|
1325
|
+
|
|
1326
|
+
// SuperGrok surfaces a few models under IDs that differ from their public
|
|
1327
|
+
// `xai` catalog equivalent, so the exact-ID price fallback misses them. Map
|
|
1328
|
+
// the OAuth ID to the paid ID it mirrors.
|
|
1329
|
+
const XAI_OAUTH_PRICE_ALIASES: Record<string, string> = {
|
|
1330
|
+
"grok-4.20-multi-agent-0309": "grok-4.20-multi-agent-beta-latest",
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
function xaiHasLongContextTier(modelId: string): boolean {
|
|
1334
|
+
return (
|
|
1335
|
+
modelId === "grok-4.3" ||
|
|
1336
|
+
modelId === "grok-4.5" ||
|
|
1337
|
+
modelId === "grok-4.6" ||
|
|
1338
|
+
modelId === "grok-build-0.1" ||
|
|
1339
|
+
modelId.startsWith("grok-4.20")
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function hasTokenPrice(cost: ModelSpec["cost"]): boolean {
|
|
1344
|
+
return cost.input !== 0 || cost.output !== 0 || cost.cacheRead !== 0 || cost.cacheWrite !== 0;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function withXaiLongContextTier(model: ModelSpec): ModelSpec {
|
|
1348
|
+
if (!xaiHasLongContextTier(model.id) || !hasTokenPrice(model.cost)) return model;
|
|
1349
|
+
const longContext: LongContextTokenCost = {
|
|
1350
|
+
inputThreshold: XAI_LONG_CONTEXT_THRESHOLD,
|
|
1351
|
+
inputThresholdInclusive: true,
|
|
1352
|
+
input: model.cost.input * XAI_LONG_CONTEXT_MULTIPLIER,
|
|
1353
|
+
output: model.cost.output * XAI_LONG_CONTEXT_MULTIPLIER,
|
|
1354
|
+
cacheRead: model.cost.cacheRead * XAI_LONG_CONTEXT_MULTIPLIER,
|
|
1355
|
+
cacheWrite: model.cost.cacheWrite * XAI_LONG_CONTEXT_MULTIPLIER,
|
|
1356
|
+
};
|
|
1357
|
+
return { ...model, cost: { ...model.cost, longContext } };
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Applies xAI's long-context rate card and mirrors exact public-model prices
|
|
1362
|
+
* onto matching SuperGrok catalog rows.
|
|
1363
|
+
*/
|
|
1364
|
+
export function applyXaiCatalogPricing(models: readonly ModelSpec[]): ModelSpec[] {
|
|
1365
|
+
const pricedModels = models.map(model => (model.provider === "xai" ? withXaiLongContextTier(model) : model));
|
|
1366
|
+
const publicCosts = new Map(
|
|
1367
|
+
pricedModels
|
|
1368
|
+
.filter(model => model.provider === "xai" && hasTokenPrice(model.cost))
|
|
1369
|
+
.map(model => [model.id, model.cost]),
|
|
1370
|
+
);
|
|
1371
|
+
|
|
1372
|
+
return pricedModels.map(model => {
|
|
1373
|
+
if (model.provider !== "xai-oauth" || hasTokenPrice(model.cost)) return model;
|
|
1374
|
+
const alias = XAI_OAUTH_PRICE_ALIASES[model.id];
|
|
1375
|
+
const publicCost = publicCosts.get(model.id) ?? (alias ? publicCosts.get(alias) : undefined);
|
|
1376
|
+
return publicCost ? { ...model, cost: { ...publicCost } } : model;
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1318
1380
|
export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
1319
1381
|
return {
|
|
1320
1382
|
...createOpenAICompatibleModelManagerOptions({
|
package/src/types.ts
CHANGED
|
@@ -847,12 +847,15 @@ export interface TokenCost {
|
|
|
847
847
|
}
|
|
848
848
|
|
|
849
849
|
/**
|
|
850
|
-
* Rates applied to the full request when its prompt exceeds `inputThreshold
|
|
851
|
-
*
|
|
852
|
-
* provider-orchestration input
|
|
850
|
+
* Rates applied to the full request when its prompt exceeds `inputThreshold`,
|
|
851
|
+
* or reaches it when `inputThresholdInclusive` is true. Prompt input is the
|
|
852
|
+
* sum of uncached, cached-read, cache-write, and provider-orchestration input
|
|
853
|
+
* tokens.
|
|
853
854
|
*/
|
|
854
855
|
export interface LongContextTokenCost extends TokenCost {
|
|
855
856
|
inputThreshold: number;
|
|
857
|
+
/** Whether the long-context tier starts exactly at `inputThreshold`. */
|
|
858
|
+
inputThresholdInclusive?: boolean;
|
|
856
859
|
}
|
|
857
860
|
|
|
858
861
|
/** Base token rates plus an optional long-context tier. */
|
package/src/variant-collapse.ts
CHANGED
|
@@ -57,6 +57,15 @@ export interface EffortVariantFamily {
|
|
|
57
57
|
* when it equals the logical id).
|
|
58
58
|
*/
|
|
59
59
|
members: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* Preferred default wire id: overrides the member-order default for the
|
|
62
|
+
* collapsed spec's `requestModelId` when this member is live. Lets a
|
|
63
|
+
* mandatory-reasoning family advertise a canonical tier that is not its
|
|
64
|
+
* numeric floor (Cursor Grok 4.5/4.6 default to `-medium`, the only tier the
|
|
65
|
+
* Start plan serves; the `-low` floor is refused). Ignored when absent from
|
|
66
|
+
* the input or retired.
|
|
67
|
+
*/
|
|
68
|
+
defaultMember?: string;
|
|
60
69
|
/**
|
|
61
70
|
* Wire ids upstream no longer serves (e.g. a deployment killed while
|
|
62
71
|
* discovery still advertises it). Fresh collapsing never routes to them,
|
|
@@ -124,7 +133,13 @@ const DEVIN_FOUR_TIER_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, E
|
|
|
124
133
|
* `mode: "effort"` (mandatory when the family has no `off` route). Shared by the
|
|
125
134
|
* Devin and Cursor tables, whose per-effort siblings follow the same shape.
|
|
126
135
|
*/
|
|
127
|
-
function tierFamily(
|
|
136
|
+
function tierFamily(
|
|
137
|
+
id: string,
|
|
138
|
+
name: string,
|
|
139
|
+
routes: TierRoutes,
|
|
140
|
+
efforts: readonly Effort[],
|
|
141
|
+
defaultMember?: string,
|
|
142
|
+
): EffortVariantFamily {
|
|
128
143
|
const routing: Partial<Record<Effort | "off", string>> = {};
|
|
129
144
|
if (routes.off) routing.off = routes.off;
|
|
130
145
|
for (const effort of efforts) {
|
|
@@ -168,6 +183,7 @@ function tierFamily(id: string, name: string, routes: TierRoutes, efforts: reado
|
|
|
168
183
|
efforts,
|
|
169
184
|
...(routes.off ? undefined : { requiresEffort: true }),
|
|
170
185
|
},
|
|
186
|
+
...(defaultMember !== undefined ? { defaultMember } : undefined),
|
|
171
187
|
};
|
|
172
188
|
}
|
|
173
189
|
|
|
@@ -719,7 +735,9 @@ const CURSOR_GPT_56_EFFORTS: readonly Effort[] = [Effort.Low, Effort.Medium, Eff
|
|
|
719
735
|
* (`cursor-grok-4.6-low|-medium|-high|-xhigh`) alongside a parallel `-fast`
|
|
720
736
|
* service-tier lane (`-low-fast`, …). Each lane collapses into its own logical
|
|
721
737
|
* model — the `-fast` axis is a sibling family, never a second routing
|
|
722
|
-
* dimension — with effort routing onto the live sibling wire ids.
|
|
738
|
+
* dimension — with effort routing onto the live sibling wire ids. The mandatory
|
|
739
|
+
* `-medium` default is the tier Cursor's Start plan serves at fixed settings;
|
|
740
|
+
* the `-low` floor is refused there (issue #9478).
|
|
723
741
|
*/
|
|
724
742
|
function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]): readonly EffortVariantFamily[] {
|
|
725
743
|
const build = (fast: boolean): EffortVariantFamily => {
|
|
@@ -728,7 +746,13 @@ function cursorGrokFamilies(version: "4.5" | "4.6", efforts: readonly Effort[]):
|
|
|
728
746
|
for (const effort of efforts) {
|
|
729
747
|
routes[effort] = `cursor-grok-${version}-${effort}${suffix}`;
|
|
730
748
|
}
|
|
731
|
-
return tierFamily(
|
|
749
|
+
return tierFamily(
|
|
750
|
+
`cursor-grok-${version}${suffix}`,
|
|
751
|
+
`Grok ${version}${fast ? " Fast" : ""}`,
|
|
752
|
+
routes,
|
|
753
|
+
efforts,
|
|
754
|
+
`cursor-grok-${version}-${Effort.Medium}${suffix}`,
|
|
755
|
+
);
|
|
732
756
|
};
|
|
733
757
|
return [build(false), build(true)];
|
|
734
758
|
}
|
|
@@ -1142,6 +1166,37 @@ function refreshCollapsedThinking<TSpec extends VariantSpecLike>(
|
|
|
1142
1166
|
return { ...spec, thinking, ...(requestModelId !== undefined ? { requestModelId } : {}) };
|
|
1143
1167
|
}
|
|
1144
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* Re-point a collapsed snapshot's default wire id to the family's declared
|
|
1171
|
+
* {@link EffortVariantFamily.defaultMember} when the snapshot still advertises a
|
|
1172
|
+
* different default. Bundled catalog and cache rows freeze `requestModelId` from
|
|
1173
|
+
* before a family gained a preferred default (Cursor Grok 4.5/4.6 → `-medium`),
|
|
1174
|
+
* and neither the existing-collapsed pass-through nor `refreshCollapsedThinking`
|
|
1175
|
+
* (scoped to `effortBudgets` families) would otherwise correct them — leaving an
|
|
1176
|
+
* effort-less request clamped to the refused `-low` tier (issue #9478). Only
|
|
1177
|
+
* re-points when the target is a live route in the snapshot's own
|
|
1178
|
+
* `effortRouting`. Returns `spec` by reference when unchanged.
|
|
1179
|
+
*/
|
|
1180
|
+
function reconcileDefaultMember<TSpec extends VariantSpecLike>(
|
|
1181
|
+
spec: TSpec,
|
|
1182
|
+
family: EffortVariantFamily,
|
|
1183
|
+
presentMembers?: ReadonlySet<string>,
|
|
1184
|
+
): TSpec {
|
|
1185
|
+
const defaultMember = family.defaultMember;
|
|
1186
|
+
if (defaultMember === undefined || defaultMember === spec.id) return spec;
|
|
1187
|
+
const target =
|
|
1188
|
+
presentMembers === undefined || presentMembers.has(defaultMember)
|
|
1189
|
+
? defaultMember
|
|
1190
|
+
: family.members.find(id => presentMembers.has(id));
|
|
1191
|
+
if (target === undefined || spec.requestModelId === target) return spec;
|
|
1192
|
+
const routing = spec.thinking?.effortRouting;
|
|
1193
|
+
if (routing === undefined) return spec;
|
|
1194
|
+
for (const key in routing) {
|
|
1195
|
+
if (routing[key as Effort | "off"] === target) return { ...spec, requestModelId: target };
|
|
1196
|
+
}
|
|
1197
|
+
return spec;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1145
1200
|
/**
|
|
1146
1201
|
* Collapse every family in `table` found in `specs`. Non-member specs pass
|
|
1147
1202
|
* through verbatim (by reference), order preserved; the collapsed spec
|
|
@@ -1182,7 +1237,7 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1182
1237
|
// Recycled extraAliases rows are healed in a later pass.
|
|
1183
1238
|
const refreshed =
|
|
1184
1239
|
existing !== undefined && existingCollapsed
|
|
1185
|
-
? refreshCollapsedThinking(reconciled ?? existing, family, retired)
|
|
1240
|
+
? reconcileDefaultMember(refreshCollapsedThinking(reconciled ?? existing, family, retired), family)
|
|
1186
1241
|
: reconciled;
|
|
1187
1242
|
if (refreshed !== undefined && refreshed !== existing) {
|
|
1188
1243
|
familyIdBySpecId.set(family.id, family.id);
|
|
@@ -1195,9 +1250,11 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1195
1250
|
if (existing) familyIdBySpecId.set(family.id, family.id);
|
|
1196
1251
|
|
|
1197
1252
|
if (existingCollapsed) {
|
|
1198
|
-
// Mixed input: the collapsed entry
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1253
|
+
// Mixed input: the collapsed entry wins; stale raw members are deduped
|
|
1254
|
+
// away. Retired targets are re-pointed first, then the default wire id
|
|
1255
|
+
// prefers the family's declared member when live and otherwise falls
|
|
1256
|
+
// back to the first member the account actually advertised.
|
|
1257
|
+
replacement.set(family.id, reconcileDefaultMember(reconciled as TSpec, family, new Set(rawPresent)));
|
|
1201
1258
|
continue;
|
|
1202
1259
|
}
|
|
1203
1260
|
|
|
@@ -1241,10 +1298,17 @@ export function collapseEffortVariants<TSpec extends VariantSpecLike>(
|
|
|
1241
1298
|
contextWindow: maxOrNull(memberSpecs.map(spec => spec.contextWindow)),
|
|
1242
1299
|
maxTokens: maxOrNull(memberSpecs.map(spec => spec.maxTokens)),
|
|
1243
1300
|
};
|
|
1244
|
-
// The default wire id is the
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1301
|
+
// The default wire id is the family's declared `defaultMember` when live,
|
|
1302
|
+
// else the highest-priority live member. Omitted when it equals the
|
|
1303
|
+
// logical id (bare/thinking pairs) — `resolveWireModelId` falls back.
|
|
1304
|
+
// Retired members never become the default.
|
|
1305
|
+
const preferredDefault =
|
|
1306
|
+
family.defaultMember !== undefined &&
|
|
1307
|
+
presentSet.has(family.defaultMember) &&
|
|
1308
|
+
!retired?.has(family.defaultMember)
|
|
1309
|
+
? family.defaultMember
|
|
1310
|
+
: undefined;
|
|
1311
|
+
const defaultWireId = preferredDefault ?? rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0];
|
|
1248
1312
|
if (defaultWireId === family.id) {
|
|
1249
1313
|
if (usedAbsentEffortRoute) {
|
|
1250
1314
|
collapsed.requestModelId = defaultWireId as string;
|