letmecode 0.1.28 → 0.1.30
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/README.md +4 -2
- package/ink-app/dist/index.js +12 -9
- package/ink-app/dist/providers/antigravity/models.js +13 -1
- package/ink-app/dist/providers/antigravity/provider.js +26 -83
- package/ink-app/dist/providers/claude.js +95 -91
- package/ink-app/dist/providers/codex.js +245 -116
- package/ink-app/dist/providers/copilot/models.js +24 -45
- package/ink-app/dist/providers/copilot/provider.js +25 -4
- package/ink-app/dist/providers/copilot/usage/aggregate.js +14 -11
- package/ink-app/dist/providers/limits.js +42 -5
- package/ink-app/dist/providers/pricing.js +116 -16
- package/ink-app/dist/reporting.js +4 -5
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { addUsageTotals, sumUsageTotals } from "../../contract.js";
|
|
2
2
|
import { addDailyUsage, buildDailyUsageRows, createDailyUsageAggregates } from "../../daily.js";
|
|
3
|
-
import { isNonBillableCopilotModel, normalizeCopilotModelId
|
|
3
|
+
import { isNonBillableCopilotModel, normalizeCopilotModelId } from "../models.js";
|
|
4
|
+
import { modelCostCredits } from "../../pricing.js";
|
|
4
5
|
/**
|
|
5
6
|
* Select events whose timestamp falls in the half-open interval
|
|
6
7
|
* `[startTimeMs, endTimeMs)`. An event exactly at `endTimeMs` belongs to the
|
|
@@ -16,7 +17,7 @@ export function filterCopilotUsageEvents(events, startTimeMs, endTimeMs) {
|
|
|
16
17
|
* reported input already INCLUDES cache-read tokens but NOT cache-write tokens.
|
|
17
18
|
* Pure and deterministic: independent of input ordering.
|
|
18
19
|
*/
|
|
19
|
-
export function aggregateCopilotUsage(events) {
|
|
20
|
+
export function aggregateCopilotUsage(events, pricing = new Map()) {
|
|
20
21
|
const byModel = new Map();
|
|
21
22
|
const byDay = createDailyUsageAggregates();
|
|
22
23
|
for (const event of events) {
|
|
@@ -35,15 +36,17 @@ export function aggregateCopilotUsage(events) {
|
|
|
35
36
|
const output = event.outputTokens;
|
|
36
37
|
const reasoning = Math.min(event.reasoningOutputTokens, output);
|
|
37
38
|
const nonBillable = isNonBillableCopilotModel(modelId);
|
|
38
|
-
const
|
|
39
|
-
|
|
39
|
+
const estimatedCredits = hasCacheInfo
|
|
40
|
+
? modelCostCredits(pricing.get(modelId), {
|
|
41
|
+
inputTokens: uncachedInput,
|
|
42
|
+
outputTokens: output,
|
|
43
|
+
cacheReadInputTokens: cacheRead,
|
|
44
|
+
cacheWrite5mInputTokens: cacheWrite,
|
|
45
|
+
cacheWrite1hInputTokens: 0
|
|
46
|
+
})
|
|
47
|
+
: undefined;
|
|
48
|
+
const creditsKnown = nonBillable || estimatedCredits !== undefined;
|
|
40
49
|
const estimatedCreditsStatus = creditsKnown ? "known" : "unavailable";
|
|
41
|
-
const estimatedCredits = rate !== undefined && hasCacheInfo
|
|
42
|
-
? (uncachedInput / 1000000) * rate.input +
|
|
43
|
-
(cacheRead / 1000000) * rate.cacheRead +
|
|
44
|
-
(cacheWrite / 1000000) * rate.cacheWrite +
|
|
45
|
-
(output / 1000000) * rate.output
|
|
46
|
-
: 0;
|
|
47
50
|
const totals = {
|
|
48
51
|
inputTokens: uncachedInput,
|
|
49
52
|
outputTokens: output,
|
|
@@ -53,7 +56,7 @@ export function aggregateCopilotUsage(events) {
|
|
|
53
56
|
cacheWrite1hInputTokens: 0,
|
|
54
57
|
reasoningOutputTokens: reasoning,
|
|
55
58
|
totalTokens: uncachedInput + cacheRead + cacheWrite + output,
|
|
56
|
-
estimatedCredits,
|
|
59
|
+
estimatedCredits: nonBillable ? 0 : (estimatedCredits ?? 0),
|
|
57
60
|
eventCount: 1,
|
|
58
61
|
cacheReadStatus: event.cacheReadStatus,
|
|
59
62
|
cacheWriteStatus: event.cacheWriteStatus,
|
|
@@ -88,6 +88,7 @@ export function buildWindowLists(windows) {
|
|
|
88
88
|
lastSeenUtcIso: formatIsoFromMilliseconds(window.lastSeenMs),
|
|
89
89
|
minUsedPercent: window.minUsedPercent,
|
|
90
90
|
maxUsedPercent: window.maxUsedPercent,
|
|
91
|
+
measuredUsedPercent: usage.measuredUsedPercent,
|
|
91
92
|
totals: usage.totals,
|
|
92
93
|
modelUsage: usage.modelUsage,
|
|
93
94
|
eventCount: 0
|
|
@@ -178,6 +179,10 @@ function collapseNearbyWindows(rows) {
|
|
|
178
179
|
existing.lastSeenUtcIso > row.lastSeenUtcIso ? existing.lastSeenUtcIso : row.lastSeenUtcIso;
|
|
179
180
|
existing.minUsedPercent = Math.min(existing.minUsedPercent, row.minUsedPercent);
|
|
180
181
|
existing.maxUsedPercent = Math.max(existing.maxUsedPercent, row.maxUsedPercent);
|
|
182
|
+
existing.measuredUsedPercent =
|
|
183
|
+
existing.minUsedPercent === existing.maxUsedPercent
|
|
184
|
+
? 0
|
|
185
|
+
: clampUsedPercent(existing.maxUsedPercent - existing.minUsedPercent);
|
|
181
186
|
addUsageTotals(existing.totals, row.totals);
|
|
182
187
|
existing.modelUsage = mergeModelUsageRows(existing.modelUsage, row.modelUsage);
|
|
183
188
|
existing.eventCount = existing.totals.eventCount;
|
|
@@ -189,13 +194,22 @@ function computeWindowUsage(events) {
|
|
|
189
194
|
// saturation has to be applied after we sort the captured window events.
|
|
190
195
|
const totals = createEmptyUsageTotals();
|
|
191
196
|
const byModel = new Map();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
const sortedEvents = [...events].sort((left, right) => left.eventTimeMs - right.eventTimeMs);
|
|
198
|
+
const baselinePercent = clampUsedPercent(sortedEvents[0]?.usedPercent ?? 0);
|
|
199
|
+
// Each rate-limit snapshot is recorded after the usage event on the same
|
|
200
|
+
// line. The first event therefore has no matching "before" percentage, even
|
|
201
|
+
// when its post-event value rounds to zero. Keep its percentage only as the
|
|
202
|
+
// baseline and aggregate requests from the next snapshot onward.
|
|
203
|
+
const firstIncludedIndex = sortedEvents.length > 0 ? 1 : 0;
|
|
204
|
+
let highestIncludedPercent = baselinePercent;
|
|
205
|
+
let sawBelowCap = baselinePercent < 100;
|
|
206
|
+
let isExhausted = baselinePercent >= 100;
|
|
207
|
+
for (const [index, event] of sortedEvents.entries()) {
|
|
195
208
|
sawBelowCap || (sawBelowCap = event.usedPercent < 100);
|
|
196
|
-
if (!isExhausted) {
|
|
209
|
+
if (index >= firstIncludedIndex && !isExhausted) {
|
|
197
210
|
addUsageTotals(totals, event.totals);
|
|
198
211
|
addWindowModelUsage(byModel, event.modelId, event.totals);
|
|
212
|
+
highestIncludedPercent = Math.max(highestIncludedPercent, clampUsedPercent(event.usedPercent));
|
|
199
213
|
if (sawBelowCap && event.usedPercent >= 100) {
|
|
200
214
|
isExhausted = true;
|
|
201
215
|
}
|
|
@@ -203,9 +217,32 @@ function computeWindowUsage(events) {
|
|
|
203
217
|
}
|
|
204
218
|
return {
|
|
205
219
|
totals,
|
|
206
|
-
modelUsage: buildModelUsageRows(byModel)
|
|
220
|
+
modelUsage: buildModelUsageRows(byModel),
|
|
221
|
+
measuredUsedPercent: clampUsedPercent(highestIncludedPercent - baselinePercent)
|
|
207
222
|
};
|
|
208
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Return the provider percentage represented by a window's token totals.
|
|
226
|
+
* Older/injected rows without the explicit field retain the legacy derivation.
|
|
227
|
+
*/
|
|
228
|
+
export function resolveMeasuredUsedPercent(window) {
|
|
229
|
+
if (window.measuredUsedPercent === null) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (typeof window.measuredUsedPercent === "number" && Number.isFinite(window.measuredUsedPercent)) {
|
|
233
|
+
return clampUsedPercent(window.measuredUsedPercent);
|
|
234
|
+
}
|
|
235
|
+
if (window.minUsedPercent === window.maxUsedPercent) {
|
|
236
|
+
return clampUsedPercent(window.maxUsedPercent);
|
|
237
|
+
}
|
|
238
|
+
return clampUsedPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
239
|
+
}
|
|
240
|
+
function clampUsedPercent(value) {
|
|
241
|
+
if (!Number.isFinite(value)) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
return Math.max(0, Math.min(100, value));
|
|
245
|
+
}
|
|
209
246
|
function upsertWindow(windows, scope, rateLimits, window, eventTimeMs, modelId, deltaTotals) {
|
|
210
247
|
if (!window) {
|
|
211
248
|
return;
|
|
@@ -1,23 +1,123 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { request as httpRequest } from "node:http";
|
|
2
|
+
import { request as httpsRequest } from "node:https";
|
|
3
|
+
const MODEL_PRICING_ENDPOINT = process.env.LETMECODE_MODEL_PRICING_ENDPOINT ??
|
|
4
|
+
"https://devforth.io/admin/adminapi/v1/get_model_pricing";
|
|
5
|
+
const PRICE_CACHE_TTL_MS = 5 * 60000;
|
|
6
|
+
const USD_TO_CREDITS = 100;
|
|
7
|
+
const MODEL_SLUG_ALIASES = {
|
|
8
|
+
"claude-haiku-4-5": "claude-4-5-haiku-reasoning",
|
|
9
|
+
"claude-sonnet-4-5": "claude-4-5-sonnet-thinking",
|
|
10
|
+
"gemini-3-1-pro": "gemini-3-1-pro-preview"
|
|
11
|
+
};
|
|
12
|
+
const responseCache = new Map();
|
|
13
|
+
let pricingTransport = postModelPricingRequest;
|
|
14
|
+
export function configureModelPricingTransport(transport) {
|
|
15
|
+
pricingTransport = transport;
|
|
16
|
+
responseCache.clear();
|
|
17
|
+
}
|
|
18
|
+
export function modelPricingSlug(modelId) {
|
|
19
|
+
const slug = modelId
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[._\s]+/g, "-")
|
|
23
|
+
.replace(/-+/g, "-")
|
|
24
|
+
.replace(/-(?:\d{8}|\d{4}-\d{2}-\d{2})$/, "");
|
|
25
|
+
return MODEL_SLUG_ALIASES[slug] ?? slug;
|
|
26
|
+
}
|
|
27
|
+
export async function fetchModelPricing(modelIds, source) {
|
|
28
|
+
const slugByModelId = new Map([...new Set(modelIds)]
|
|
29
|
+
.filter((modelId) => modelId !== "unknown" && modelId !== "<synthetic>")
|
|
30
|
+
.map((modelId) => [modelId, modelPricingSlug(modelId)]));
|
|
31
|
+
const slugs = [...new Set(slugByModelId.values())].sort();
|
|
32
|
+
if (slugs.length === 0) {
|
|
33
|
+
return new Map();
|
|
34
|
+
}
|
|
35
|
+
const request = {
|
|
36
|
+
slugs,
|
|
37
|
+
available_in: { main: [source], other: [] }
|
|
38
|
+
};
|
|
39
|
+
const response = await fetchCached(request);
|
|
40
|
+
const pricingBySlug = new Map(response.models.map((model) => [
|
|
41
|
+
model.slug,
|
|
42
|
+
{
|
|
43
|
+
input: model.input,
|
|
44
|
+
output: model.output,
|
|
45
|
+
inputCacheRead: model.input_cache_read,
|
|
46
|
+
inputCacheWrite5m: model.input_cache_w5m,
|
|
47
|
+
inputCacheWrite1h: model.input_cache_w1h
|
|
48
|
+
}
|
|
49
|
+
]));
|
|
50
|
+
return new Map([...slugByModelId.entries()].flatMap(([modelId, slug]) => {
|
|
51
|
+
const pricing = pricingBySlug.get(slug);
|
|
52
|
+
return pricing ? [[modelId, pricing]] : [];
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
export function modelCostCredits(pricing, usage) {
|
|
56
|
+
if (!pricing) {
|
|
8
57
|
return undefined;
|
|
9
58
|
}
|
|
10
|
-
|
|
11
|
-
if (!rate) {
|
|
59
|
+
if (usage.cacheWrite5mInputTokens > 0 && pricing.inputCacheWrite5m === null) {
|
|
12
60
|
return undefined;
|
|
13
61
|
}
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
62
|
+
if (usage.cacheWrite1hInputTokens > 0 && pricing.inputCacheWrite1h === null) {
|
|
63
|
+
return undefined;
|
|
16
64
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
|
70
|
+
}
|
|
71
|
+
async function fetchCached(request) {
|
|
72
|
+
const key = JSON.stringify(request);
|
|
73
|
+
const cached = responseCache.get(key);
|
|
74
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
75
|
+
return cached.response;
|
|
76
|
+
}
|
|
77
|
+
const response = pricingTransport(request);
|
|
78
|
+
responseCache.set(key, {
|
|
79
|
+
expiresAt: Date.now() + PRICE_CACHE_TTL_MS,
|
|
80
|
+
response
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
return await response;
|
|
21
84
|
}
|
|
22
|
-
|
|
85
|
+
catch (error) {
|
|
86
|
+
responseCache.delete(key);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function postModelPricingRequest(body) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const encodedBody = Buffer.from(JSON.stringify(body), "utf8");
|
|
93
|
+
const target = new URL(MODEL_PRICING_ENDPOINT);
|
|
94
|
+
const request = target.protocol === "http:" ? httpRequest : httpsRequest;
|
|
95
|
+
const req = request({
|
|
96
|
+
method: "POST",
|
|
97
|
+
protocol: target.protocol,
|
|
98
|
+
hostname: target.hostname,
|
|
99
|
+
port: target.port,
|
|
100
|
+
path: `${target.pathname}${target.search}`,
|
|
101
|
+
headers: {
|
|
102
|
+
"content-type": "application/json",
|
|
103
|
+
"content-length": encodedBody.byteLength
|
|
104
|
+
}
|
|
105
|
+
}, (res) => {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
108
|
+
res.on("end", () => {
|
|
109
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
110
|
+
reject(new Error(`Model pricing request failed with status ${res.statusCode ?? "unknown"}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
req.on("error", reject);
|
|
117
|
+
req.setTimeout(5000, () => {
|
|
118
|
+
req.destroy(new Error("Model pricing request timed out"));
|
|
119
|
+
});
|
|
120
|
+
req.write(encodedBody);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
23
123
|
}
|
|
@@ -2,6 +2,7 @@ import { request } from "node:https";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { resolveMeasuredUsedPercent } from "./providers/limits.js";
|
|
5
6
|
const REPORTING_ENDPOINT = "https://devforth.io/admin/api/report_ussage_anonymous";
|
|
6
7
|
const CREDIT_TO_DOLLARS = 0.01;
|
|
7
8
|
// Limit windows at or below this used-percent carry too little signal to be
|
|
@@ -99,13 +100,11 @@ function buildUsageRaw(modelUsage) {
|
|
|
99
100
|
return usageRaw;
|
|
100
101
|
}
|
|
101
102
|
function resolveReportedUsedPercents(window) {
|
|
102
|
-
|
|
103
|
-
return clampPercent(window.maxUsedPercent);
|
|
104
|
-
}
|
|
105
|
-
return clampPercent(window.maxUsedPercent - window.minUsedPercent);
|
|
103
|
+
return clampPercent(resolveMeasuredUsedPercent(window) ?? 0);
|
|
106
104
|
}
|
|
107
105
|
function shouldReportUsageWindow(window) {
|
|
108
|
-
return
|
|
106
|
+
return (window.totals.estimatedCreditsStatus !== "unavailable" &&
|
|
107
|
+
resolveReportedUsedPercents(window) > SKIP_REPORT_USED_PERCENTS);
|
|
109
108
|
}
|
|
110
109
|
function clampPercent(value) {
|
|
111
110
|
if (!Number.isFinite(value)) {
|