@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/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: Model["cost"], promptInputTokens: number): TokenCost {
46
+ function resolveTokenCost(cost: ModelCost, promptInputTokens: number): TokenCost {
47
47
  const longContext = cost.longContext;
48
48
  if (!longContext) return cost;
49
- return promptInputTokens > longContext.inputThreshold ? longContext : cost;
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: Model["cost"], promptInputTokens: number): number {
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
- export function calculateCost<TApi extends Api>(model: Model<TApi>, usage: Usage): Usage["cost"] {
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(model.cost, promptInputTokens);
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
- // v3: max-mode Claude/Gemini rows cached before the 1M context-window
58
- // discovery fix carry a stale 200k window and must be refetched.
59
- return "cursor:max-mode-v3";
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
- * Prompt input is the sum of uncached, cached-read, cache-write, and
852
- * provider-orchestration input tokens.
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. */
@@ -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(id: string, name: string, routes: TierRoutes, efforts: readonly Effort[]): EffortVariantFamily {
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(`cursor-grok-${version}${suffix}`, `Grok ${version}${fast ? " Fast" : ""}`, routes, efforts);
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 (live truth) wins; stale raw
1199
- // members are deduped away. Retired targets are re-pointed first.
1200
- replacement.set(family.id, reconciled as TSpec);
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 highest-priority live member; omit when it
1245
- // equals the logical id (bare/thinking pairs) — `resolveWireModelId`
1246
- // falls back. Retired members never become the default.
1247
- const defaultWireId = rawPresent.find(id => !retired?.has(id)) ?? rawPresent[0];
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;