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
|
-
|
|
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
|
-
|
|
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 &&
|
|
79
|
+
if (usage.cacheWrite1hInputTokens > 0 && rates.inputCacheWrite1h === null) {
|
|
63
80
|
return undefined;
|
|
64
81
|
}
|
|
65
|
-
return ((usage.inputTokens / 1000000) *
|
|
66
|
-
(usage.cacheReadInputTokens / 1000000) *
|
|
67
|
-
(usage.cacheWrite5mInputTokens / 1000000) * (
|
|
68
|
-
(usage.cacheWrite1hInputTokens / 1000000) * (
|
|
69
|
-
(usage.outputTokens / 1000000) *
|
|
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);
|