letmecode 0.1.30 → 0.1.32

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.
@@ -19,7 +19,12 @@ export function selectLatestActiveLimitWindows(windows, nowMs = Date.now()) {
19
19
  continue;
20
20
  }
21
21
  const startMs = Date.parse(window.startTimeUtcIso);
22
- const groupKey = JSON.stringify([window.planType, window.windowMinutes]);
22
+ // `limitId` is part of the key because a plan can run several limits of the
23
+ // same duration side by side: Claude meters an all-models week and a
24
+ // model-family week that share a plan, a length and a reset instant. Keyed
25
+ // on plan and duration alone they collapse into one group and the family
26
+ // window loses the tie, so it never counts as active.
27
+ const groupKey = JSON.stringify([window.planType, window.limitId, window.windowMinutes]);
23
28
  const current = latestByPlanAndWindow.get(groupKey);
24
29
  if (!current || startMs > current.startMs) {
25
30
  latestByPlanAndWindow.set(groupKey, { window, startMs });
@@ -44,7 +44,17 @@ export async function fetchModelPricing(modelIds, source) {
44
44
  output: model.output,
45
45
  inputCacheRead: model.input_cache_read,
46
46
  inputCacheWrite5m: model.input_cache_w5m,
47
- inputCacheWrite1h: model.input_cache_w1h
47
+ inputCacheWrite1h: model.input_cache_w1h,
48
+ longContext: model.long_context
49
+ ? {
50
+ inputTokensThreshold: model.long_context.input_tokens_threshold,
51
+ input: model.long_context.input,
52
+ output: model.long_context.output,
53
+ inputCacheRead: model.long_context.input_cache_read,
54
+ inputCacheWrite5m: model.long_context.input_cache_w5m,
55
+ inputCacheWrite1h: model.long_context.input_cache_w1h
56
+ }
57
+ : null
48
58
  }
49
59
  ]));
50
60
  return new Map([...slugByModelId.entries()].flatMap(([modelId, slug]) => {
@@ -56,17 +66,24 @@ export function modelCostCredits(pricing, usage) {
56
66
  if (!pricing) {
57
67
  return undefined;
58
68
  }
59
- if (usage.cacheWrite5mInputTokens > 0 && pricing.inputCacheWrite5m === null) {
69
+ const totalInputTokens = usage.inputTokens +
70
+ usage.cacheReadInputTokens +
71
+ usage.cacheWrite5mInputTokens +
72
+ usage.cacheWrite1hInputTokens;
73
+ const rates = pricing.longContext && totalInputTokens > pricing.longContext.inputTokensThreshold
74
+ ? pricing.longContext
75
+ : pricing;
76
+ if (usage.cacheWrite5mInputTokens > 0 && rates.inputCacheWrite5m === null) {
60
77
  return undefined;
61
78
  }
62
- if (usage.cacheWrite1hInputTokens > 0 && pricing.inputCacheWrite1h === null) {
79
+ if (usage.cacheWrite1hInputTokens > 0 && rates.inputCacheWrite1h === null) {
63
80
  return undefined;
64
81
  }
65
- return ((usage.inputTokens / 1000000) * pricing.input +
66
- (usage.cacheReadInputTokens / 1000000) * pricing.inputCacheRead +
67
- (usage.cacheWrite5mInputTokens / 1000000) * (pricing.inputCacheWrite5m ?? 0) +
68
- (usage.cacheWrite1hInputTokens / 1000000) * (pricing.inputCacheWrite1h ?? 0) +
69
- (usage.outputTokens / 1000000) * pricing.output) * USD_TO_CREDITS;
82
+ return ((usage.inputTokens / 1000000) * rates.input +
83
+ (usage.cacheReadInputTokens / 1000000) * rates.inputCacheRead +
84
+ (usage.cacheWrite5mInputTokens / 1000000) * (rates.inputCacheWrite5m ?? 0) +
85
+ (usage.cacheWrite1hInputTokens / 1000000) * (rates.inputCacheWrite1h ?? 0) +
86
+ (usage.outputTokens / 1000000) * rates.output) * USD_TO_CREDITS;
70
87
  }
71
88
  async function fetchCached(request) {
72
89
  const key = JSON.stringify(request);
@@ -8,6 +8,11 @@ const CREDIT_TO_DOLLARS = 0.01;
8
8
  // Limit windows at or below this used-percent carry too little signal to be
9
9
  // worth reporting, so they are dropped from the anonymous usage payload.
10
10
  const SKIP_REPORT_USED_PERCENTS = 3;
11
+ // Name of the catch-all limit class: the window meters every model the plan
12
+ // offers, rather than one carved-out family.
13
+ const ALL_LIMIT_CLASS = "All";
14
+ // Mirrors the reporting endpoint's schema, which caps `limit_class` at 64.
15
+ const LIMIT_CLASS_MAX_LENGTH = 64;
11
16
  let versionCache = null;
12
17
  export async function reportAnonymousUsage(statsList) {
13
18
  const payload = await buildAnonymousUsagePayload(statsList);
@@ -36,6 +41,7 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
36
41
  return {
37
42
  agent: stats.analytics?.agentName ?? stats.providerLabel.replace(/\s+/g, ""),
38
43
  model_type: resolveReportModelType(stats, window),
44
+ limit_class: resolveReportLimitClass(stats, window),
39
45
  userid_hash: stats.analytics?.userIdHash ?? "",
40
46
  plan_id: window.planType,
41
47
  window_duration_seconds: window.windowMinutes * 60,
@@ -48,6 +54,52 @@ function buildAnonymousUsageReport(stats, window, letmecodeVersion) {
48
54
  letmecode_version: letmecodeVersion
49
55
  };
50
56
  }
57
+ /**
58
+ * Canonical, display-ready name of the limit class a window belongs to.
59
+ * Every provider words the same idea differently - Antigravity separates Gemini
60
+ * from third-party models, Claude carves a model family out of the weekly
61
+ * window - so letmecode folds them onto one vocabulary and consumers never have
62
+ * to learn the provider-specific spellings in `model_type`.
63
+ *
64
+ * The vocabulary is open: a class nobody has seen before is reported under its
65
+ * own name rather than forced into an existing bucket.
66
+ */
67
+ function resolveReportLimitClass(stats, window) {
68
+ if (stats.providerId === "claude") {
69
+ const modelFamily = window.modelType?.toLowerCase().replace(/\s+only$/, "").trim();
70
+ return modelFamily ? toLimitClassName(modelFamily) : ALL_LIMIT_CLASS;
71
+ }
72
+ if (stats.providerId === "antigravity") {
73
+ const limitId = window.limitId.toLowerCase();
74
+ if (limitId.includes("gemini")) {
75
+ return "Main";
76
+ }
77
+ if (limitId.startsWith("3p") || limitId.includes("third-party")) {
78
+ return "Other";
79
+ }
80
+ return toLimitClassName(window.limitId);
81
+ }
82
+ if (stats.providerId === "copilot") {
83
+ // Copilot quota labels ("AI Credits", "Premium requests") are already the
84
+ // name of the bucket they meter.
85
+ return window.modelType ? toLimitClassName(window.modelType) : ALL_LIMIT_CLASS;
86
+ }
87
+ // Codex, and anything else, meters every model against one shared allowance.
88
+ return ALL_LIMIT_CLASS;
89
+ }
90
+ /**
91
+ * Title-case a provider string into a class name, leaving words that already
92
+ * carry capitals alone so acronyms such as "AI Credits" survive intact.
93
+ */
94
+ function toLimitClassName(value) {
95
+ const name = value
96
+ .trim()
97
+ .split(/[\s_-]+/)
98
+ .filter(Boolean)
99
+ .map((word) => (/[A-Z]/.test(word) ? word : word.charAt(0).toUpperCase() + word.slice(1)))
100
+ .join(" ");
101
+ return name ? truncateSchemaString(name, LIMIT_CLASS_MAX_LENGTH) : ALL_LIMIT_CLASS;
102
+ }
51
103
  function resolveReportModelType(stats, window) {
52
104
  if (stats.providerId === "antigravity") {
53
105
  return resolveAntigravityReportModelType(stats, window);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letmecode",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "Terminal AI usage dashboard for Codex, Claude, Copilot, and Antigravity.",
5
5
  "author": "Devforth (https://devforth.io)",
6
6
  "license": "MIT",