aegis-desktop 0.7.3 → 0.7.5
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/package.json +1 -1
- package/renderer/app.js +88 -11
- package/renderer/usage.js +163 -7
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.5",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/renderer/app.js
CHANGED
|
@@ -274,12 +274,31 @@ function foldRoll(sessionId, usage, opts = {}) {
|
|
|
274
274
|
* Paint the topbar meter. Hidden while a session has accounted for nothing —
|
|
275
275
|
* an unused thread must not display a `0 tok` it never measured — and shown
|
|
276
276
|
* the moment a turn is folded in.
|
|
277
|
+
*
|
|
278
|
+
* `live`, when given, previews the in-flight turn on top of the session's
|
|
279
|
+
* already-folded total — text estimated the same way `foldRoll` estimates a
|
|
280
|
+
* turn the wire never reported on — WITHOUT folding it: the preview roll
|
|
281
|
+
* returned by `rollTurn` here is thrown away every frame and `rollsBySession`
|
|
282
|
+
* is never written to, so the real `foldRoll` at completion still starts from
|
|
283
|
+
* the untouched persisted total and cannot double-count this turn. (`turns`/
|
|
284
|
+
* `calls` are left at their default +1 rather than forced to 0 — `fmtRoll`
|
|
285
|
+
* treats a roll with both at 0 as "nothing counted yet" and blanks the line,
|
|
286
|
+
* which hid the preview entirely.) Before this, the meter held the previous
|
|
287
|
+
* turn's total frozen for the whole reply and only jumped at the end, which
|
|
288
|
+
* read as "the counter is dead while the AI works".
|
|
277
289
|
*/
|
|
278
|
-
function renderRollMeter(sessionId) {
|
|
290
|
+
function renderRollMeter(sessionId, live) {
|
|
279
291
|
const el = els.sessionMeter;
|
|
280
292
|
if (!el) return;
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
if (currentSessionId !== sessionId) { el.hidden = true; return; }
|
|
294
|
+
let roll = rollsBySession.get(sessionId || '');
|
|
295
|
+
if (live) {
|
|
296
|
+
roll = rollTurn(roll || emptyRoll(), undefined, {
|
|
297
|
+
prompt: live.prompt,
|
|
298
|
+
reply: live.reply,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const line = roll ? fmtRoll(roll) : '';
|
|
283
302
|
el.textContent = line;
|
|
284
303
|
el.hidden = !line;
|
|
285
304
|
if (line) el.title = 'This session, counted the way the CLI counts it — every turn rolled into one running total';
|
|
@@ -296,14 +315,24 @@ function renderRollMeter(sessionId) {
|
|
|
296
315
|
* and `costUsd` into the very same file — came back with its full total. That
|
|
297
316
|
* asymmetry is the accounting difference, not the rendering of it.
|
|
298
317
|
*
|
|
299
|
-
*
|
|
318
|
+
* One authority writes the row: `ledgerRow` in usage.js, which mirrors the
|
|
319
|
+
* CLI's `appendHistory` shape exactly. A turn the wire did not report on is
|
|
320
|
+
* STILL written — as the CLI writes it, an estimate from the turn's own text
|
|
321
|
+
* marked `real: false` — because that is what keeps the live roll and the
|
|
322
|
+
* rebuilt roll the same number. Only a dispatch with neither reported usage
|
|
323
|
+
* nor any text to estimate from writes nothing: a fabricated
|
|
300
324
|
* `{input: 0, output: 0}` row would read as a measured zero forever after,
|
|
301
325
|
* which is the one lie the token meter was built to avoid.
|
|
302
326
|
*/
|
|
303
|
-
function ledgerFields(usage, model, turn) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
function ledgerFields(usage, model, turn, text) {
|
|
328
|
+
const row = ledgerRow(usage, turn, {
|
|
329
|
+
model,
|
|
330
|
+
costUsd: turn && turn.real && typeof turn.cost === 'number' ? turn.cost : undefined,
|
|
331
|
+
calls: text && text.calls,
|
|
332
|
+
prompt: text && text.prompt,
|
|
333
|
+
reply: text && text.reply,
|
|
334
|
+
});
|
|
335
|
+
const fields = row ? Object.assign({}, row) : {};
|
|
307
336
|
if (model) fields.model = model;
|
|
308
337
|
return fields;
|
|
309
338
|
}
|
|
@@ -2101,6 +2130,11 @@ async function spawnPath(card, spec) {
|
|
|
2101
2130
|
costUsd: data && typeof data.costUsd === 'number' ? data.costUsd : undefined,
|
|
2102
2131
|
calls: data && data.calls,
|
|
2103
2132
|
turns: 0,
|
|
2133
|
+
// The dispatch's own prompt and stream, for the same reason as the turn
|
|
2134
|
+
// site: a path that reports no usage is estimated from its own text and
|
|
2135
|
+
// counted, instead of leaving the lane's calls out of the session total.
|
|
2136
|
+
prompt: `Original request:\n${spec.prompt}\n\n${spec.path.hint}`,
|
|
2137
|
+
reply: text,
|
|
2104
2138
|
});
|
|
2105
2139
|
const rollLine = fmtRoll(roll);
|
|
2106
2140
|
if (rollLine) bits.push(`session: ${rollLine}`);
|
|
@@ -3131,6 +3165,10 @@ async function send() {
|
|
|
3131
3165
|
}
|
|
3132
3166
|
const bodyEl = pendingEl.querySelector('.body');
|
|
3133
3167
|
if (bodyEl && bodyEl.textContent !== streamedText) bodyEl.textContent = streamedText;
|
|
3168
|
+
// Live estimate so the topbar meter keeps moving while the reply streams
|
|
3169
|
+
// in, instead of sitting frozen on the previous turn's total until this
|
|
3170
|
+
// one resolves — see renderRollMeter's `live` param.
|
|
3171
|
+
renderRollMeter(sessionId, { prompt, reply: streamedText });
|
|
3134
3172
|
stickToBottom();
|
|
3135
3173
|
});
|
|
3136
3174
|
|
|
@@ -3205,6 +3243,14 @@ async function send() {
|
|
|
3205
3243
|
costUsd: data && typeof data.costUsd === 'number' ? data.costUsd : undefined,
|
|
3206
3244
|
});
|
|
3207
3245
|
if (turn.tokens != null) bits.push(`tokens: ${turn.tokens}`);
|
|
3246
|
+
// A turn the wire did not report on still gets a figure — the same text
|
|
3247
|
+
// estimate that goes into the session total, marked `~` so an inferred
|
|
3248
|
+
// count is never read as a reported one. Printing nothing here while the
|
|
3249
|
+
// session total moved was the other half of "the counter looks dead".
|
|
3250
|
+
else {
|
|
3251
|
+
const est = estimatedBuckets(prompt, text);
|
|
3252
|
+
if (est) bits.push(`~${est.input + est.output} tokens`);
|
|
3253
|
+
}
|
|
3208
3254
|
if (turn.cost != null) bits.push(fmtCost(turn.cost, turn.real));
|
|
3209
3255
|
// …AND the running session total beside it, which is the number the CLI
|
|
3210
3256
|
// prints. The per-turn figure answers "what did that call cost"; only the
|
|
@@ -3216,6 +3262,11 @@ async function send() {
|
|
|
3216
3262
|
model,
|
|
3217
3263
|
costUsd: data && typeof data.costUsd === 'number' ? data.costUsd : undefined,
|
|
3218
3264
|
calls: data && data.calls,
|
|
3265
|
+
// The turn's own text, used ONLY when the wire reported no usage — so
|
|
3266
|
+
// such a turn is estimated and counted rather than dropped. This is the
|
|
3267
|
+
// CLI's `appendHistory` rule and the reason the total moves every turn.
|
|
3268
|
+
prompt,
|
|
3269
|
+
reply: text,
|
|
3219
3270
|
});
|
|
3220
3271
|
const rollLine = fmtRoll(roll);
|
|
3221
3272
|
if (rollLine) bits.push(`session: ${rollLine}`);
|
|
@@ -3228,7 +3279,11 @@ async function send() {
|
|
|
3228
3279
|
// The turn's ledger fields, so this window's spend survives the window
|
|
3229
3280
|
// — see ledgerFields. This is what makes the rolling meter the same
|
|
3230
3281
|
// quantity after a reopen as it was before one.
|
|
3231
|
-
...ledgerFields(data && data.usage, model, turn
|
|
3282
|
+
...ledgerFields(data && data.usage, model, turn, {
|
|
3283
|
+
prompt,
|
|
3284
|
+
reply: text,
|
|
3285
|
+
calls: data && data.calls,
|
|
3286
|
+
}),
|
|
3232
3287
|
});
|
|
3233
3288
|
await sync.save({ id: sessionId, title: prompt.slice(0, 60) });
|
|
3234
3289
|
} catch {
|
|
@@ -3252,9 +3307,31 @@ async function send() {
|
|
|
3252
3307
|
if (isCancellation(err, { userStopped })) {
|
|
3253
3308
|
const text = streamedText || reasoningText || '(stopped before any output)';
|
|
3254
3309
|
threadMessages.push({ role: 'assistant', content: text });
|
|
3255
|
-
|
|
3310
|
+
// A stopped turn is a real exchange and the CLI records one: its
|
|
3311
|
+
// `appendHistory` writes a `status: 'stopped'` entry for every stopped
|
|
3312
|
+
// turn, and `aggregateSessionUsage` sums it like any other. The desktop
|
|
3313
|
+
// wrote `{role, content}` and folded nothing, so an Escape mid-answer
|
|
3314
|
+
// left the session total standing still on a turn the provider had
|
|
3315
|
+
// already billed. Folded here like any other turn; with no wire usage
|
|
3316
|
+
// on this path the figure is the text estimate, marked `est` — and
|
|
3317
|
+
// never a fabricated zero.
|
|
3318
|
+
const turn = turnAccounting(undefined, model, {});
|
|
3319
|
+
const roll = foldRoll(sessionId, undefined, { model, prompt, reply: text });
|
|
3320
|
+
const stopBits = ['stopped by you'];
|
|
3321
|
+
if (turn.tokens != null) stopBits.push(`tokens: ${turn.tokens}`);
|
|
3322
|
+
else {
|
|
3323
|
+
const est = estimatedBuckets(prompt, text);
|
|
3324
|
+
if (est) stopBits.push(`~${est.input + est.output} tokens`);
|
|
3325
|
+
}
|
|
3326
|
+
const stopRollLine = fmtRoll(roll);
|
|
3327
|
+
if (stopRollLine) stopBits.push(`session: ${stopRollLine}`);
|
|
3328
|
+
addMessage('assistant', text, stopBits.join(' · '), sessionId, toolLog);
|
|
3256
3329
|
try {
|
|
3257
|
-
await sync.append(sessionId, {
|
|
3330
|
+
await sync.append(sessionId, {
|
|
3331
|
+
role: 'assistant',
|
|
3332
|
+
content: text,
|
|
3333
|
+
...ledgerFields(undefined, model, turn, { prompt, reply: text }),
|
|
3334
|
+
});
|
|
3258
3335
|
} catch {
|
|
3259
3336
|
/* persistence is non-fatal */
|
|
3260
3337
|
}
|
package/renderer/usage.js
CHANGED
|
@@ -66,6 +66,56 @@ function usageTokens(usage) {
|
|
|
66
66
|
return (input || 0) + (output || 0);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Rough token count for text the wire never measured — a direct port of the
|
|
71
|
+
* CLI's own estimator (`aegiscodex-dev/src/tokens.js estimateTokens`), and the
|
|
72
|
+
* reason its session total is MONOTONIC.
|
|
73
|
+
*
|
|
74
|
+
* This is the whole difference being fixed. The CLI's `appendHistory`
|
|
75
|
+
* (aegiscodex-dev/src/history.js) writes a `tokens` object for EVERY finished
|
|
76
|
+
* exchange: `{input, output, cacheRead, cacheWrite, real: true}` when the wire
|
|
77
|
+
* reported usage, and `{input: estimateTokens(prompt), output:
|
|
78
|
+
* estimateTokens(reply), real: false}` when it did not. `real` is a FLAG, not a
|
|
79
|
+
* gate — `aggregateSessionUsage` adds `t.input || 0` for every row regardless,
|
|
80
|
+
* so a turn without usage still moves the total and only marks it as partly
|
|
81
|
+
* estimated.
|
|
82
|
+
*
|
|
83
|
+
* The desktop rolled only reported usage and dropped everything else, so its
|
|
84
|
+
* total was flat across any turn the pool did not report on — the meter looked
|
|
85
|
+
* dead while the conversation was being billed. ~4 characters per token, the
|
|
86
|
+
* CLI's heuristic, kept identical so the two surfaces estimate the same turn
|
|
87
|
+
* the same way. Empty text is 0, never the `Math.max(1, …)` floor.
|
|
88
|
+
*/
|
|
89
|
+
function estimateTokens(text) {
|
|
90
|
+
if (!text) return 0;
|
|
91
|
+
return Math.max(1, Math.ceil([...String(text)].length / 4));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The bucket shape an exchange gets when the wire reported no usage: the CLI's
|
|
96
|
+
* `{input: estimateTokens(prompt), output: estimateTokens(reply)}`.
|
|
97
|
+
*
|
|
98
|
+
* `null` when there is no text at all to estimate from, so a dispatch that
|
|
99
|
+
* genuinely reported nothing (no usage, no prompt, no reply) still lands in
|
|
100
|
+
* `unknown` instead of being handed a fabricated zero.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} [prompt]
|
|
103
|
+
* @param {string} [reply]
|
|
104
|
+
* @returns {{input: number, output: number, cacheRead: number, cacheWrite: number}|null}
|
|
105
|
+
*/
|
|
106
|
+
function estimatedBuckets(prompt, reply) {
|
|
107
|
+
const hasPrompt = typeof prompt === 'string' && prompt.length > 0;
|
|
108
|
+
const hasReply = typeof reply === 'string' && reply.length > 0;
|
|
109
|
+
if (!hasPrompt && !hasReply) return null;
|
|
110
|
+
return {
|
|
111
|
+
input: estimateTokens(prompt),
|
|
112
|
+
output: estimateTokens(reply),
|
|
113
|
+
cacheRead: 0,
|
|
114
|
+
cacheWrite: 0,
|
|
115
|
+
real: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
69
119
|
// Per-million-token USD rates. Cache-read/write matter for long sessions.
|
|
70
120
|
//
|
|
71
121
|
// These are the PROVIDER's rates, for the fallback path where a turn has no
|
|
@@ -239,6 +289,12 @@ function emptyRoll() {
|
|
|
239
289
|
cacheWrite: 0,
|
|
240
290
|
/** Dispatches that reported no usage at all — the honest gap in `tokens`. */
|
|
241
291
|
unknown: 0,
|
|
292
|
+
/**
|
|
293
|
+
* Exchanges folded from TEXT rather than reported usage, on the CLI's
|
|
294
|
+
* `real: false` rule. They are counted in `tokens` — that is the point —
|
|
295
|
+
* and named here so an estimated figure is never read as a measured one.
|
|
296
|
+
*/
|
|
297
|
+
estimated: 0,
|
|
242
298
|
/** Settled charges (server-settled `costUsd`), rolled. */
|
|
243
299
|
cost: 0,
|
|
244
300
|
/** Locally priced turns, rolled. Never mixed into `cost`. */
|
|
@@ -253,11 +309,14 @@ function emptyRoll() {
|
|
|
253
309
|
*
|
|
254
310
|
* @param {object} [roll] the roll so far (emptyRoll() when omitted)
|
|
255
311
|
* @param {object} [usage] the response's `usage` object
|
|
256
|
-
* @param {{model?: string, costUsd?: number, calls?: number, turns?: number
|
|
312
|
+
* @param {{model?: string, costUsd?: number, calls?: number, turns?: number,
|
|
313
|
+
* prompt?: string, reply?: string, estimated?: boolean}} [opts]
|
|
257
314
|
* `calls` defaults to 1; a pooled turn may report how many provider
|
|
258
315
|
* calls it actually made. `turns: 0` folds a dispatch that is not a
|
|
259
316
|
* turn of its own — the discovery-lane card, which bills like any other
|
|
260
|
-
* call but is not something the user asked for.
|
|
317
|
+
* call but is not something the user asked for. `prompt`/`reply` are the
|
|
318
|
+
* turn's text, used ONLY when the wire reported no usage, so the turn is
|
|
319
|
+
* estimated rather than dropped (the CLI's `appendHistory` rule).
|
|
261
320
|
* @returns {object} the new roll
|
|
262
321
|
*/
|
|
263
322
|
function rollTurn(roll, usage, opts = {}) {
|
|
@@ -265,8 +324,31 @@ function rollTurn(roll, usage, opts = {}) {
|
|
|
265
324
|
next.turns += opts.turns === undefined ? 1 : Number(opts.turns) || 0;
|
|
266
325
|
next.calls += opts.calls === undefined ? 1 : Number(opts.calls) || 0;
|
|
267
326
|
const turn = turnAccounting(usage, opts.model, { costUsd: opts.costUsd });
|
|
327
|
+
// A row folded from text rather than from the wire: either this turn's own
|
|
328
|
+
// fallback below, or a stored ledger row carrying `real: false` — the shape
|
|
329
|
+
// the CLI's `appendHistory` writes for an exchange the provider did not
|
|
330
|
+
// report on.
|
|
331
|
+
const estimated = opts.estimated === true || (usage && usage.real === false);
|
|
268
332
|
if (turn.tokens == null) {
|
|
269
|
-
|
|
333
|
+
// No reported usage. The CLI does not let such a turn vanish from the
|
|
334
|
+
// total: `appendHistory` estimates it from the text and marks it
|
|
335
|
+
// `real: false` (aegiscodex-dev/src/history.js), and
|
|
336
|
+
// `aggregateSessionUsage` then adds it like any other row. Folding the same
|
|
337
|
+
// estimate here is what makes the live roll and the rebuilt roll the same
|
|
338
|
+
// number, and what stops the total standing still on exactly the turns the
|
|
339
|
+
// pool declined to report on — the symptom this fallback exists to remove.
|
|
340
|
+
const est = estimatedBuckets(opts.prompt, opts.reply);
|
|
341
|
+
if (!est) {
|
|
342
|
+
// Nothing reported AND no text to estimate from. The single case that
|
|
343
|
+
// stays uncounted: `unknown` names the gap, where a fabricated zero would
|
|
344
|
+
// read as a measurement.
|
|
345
|
+
next.unknown += 1;
|
|
346
|
+
return next;
|
|
347
|
+
}
|
|
348
|
+
next.tokens += est.input + est.output;
|
|
349
|
+
next.input += est.input;
|
|
350
|
+
next.output += est.output;
|
|
351
|
+
next.estimated += 1;
|
|
270
352
|
return next;
|
|
271
353
|
}
|
|
272
354
|
const b = usageBuckets(usage);
|
|
@@ -275,6 +357,14 @@ function rollTurn(roll, usage, opts = {}) {
|
|
|
275
357
|
next.output += b.output;
|
|
276
358
|
next.cacheRead += b.cacheRead;
|
|
277
359
|
next.cacheWrite += b.cacheWrite;
|
|
360
|
+
if (estimated) {
|
|
361
|
+
// Money stays out of an estimated exchange, deliberately and in both
|
|
362
|
+
// directions: the CLI writes no `costUsd` for one, so pricing it here would
|
|
363
|
+
// make the live roll drift from the rebuilt one — and pricing tokens that
|
|
364
|
+
// were themselves guessed would stack one guess on another.
|
|
365
|
+
next.estimated += 1;
|
|
366
|
+
return next;
|
|
367
|
+
}
|
|
278
368
|
if (turn.real) next.cost += turn.cost;
|
|
279
369
|
else if (turn.cost != null) next.estimate += turn.cost;
|
|
280
370
|
return next;
|
|
@@ -307,6 +397,55 @@ function rollMessages(messages) {
|
|
|
307
397
|
return roll;
|
|
308
398
|
}
|
|
309
399
|
|
|
400
|
+
/**
|
|
401
|
+
* The ledger row one finished dispatch must carry into the shared session
|
|
402
|
+
* store, so the rolling total can be REBUILT when the thread is reopened —
|
|
403
|
+
* the desktop's counterpart of the CLI's `appendHistory`
|
|
404
|
+
* (aegiscodex-dev/src/history.js:36).
|
|
405
|
+
*
|
|
406
|
+
* One authority for the row, on purpose. The CLI writes a `tokens` object for
|
|
407
|
+
* EVERY exchange and skips none:
|
|
408
|
+
*
|
|
409
|
+
* tokens: usage
|
|
410
|
+
* ? { input, output, cacheRead, cacheWrite, real: true }
|
|
411
|
+
* : { input: estimateTokens(prompt), output: estimateTokens(reply), real: false }
|
|
412
|
+
*
|
|
413
|
+
* …and `aggregateSessionUsage` then sums `t.input || 0` over every entry with
|
|
414
|
+
* `real` as a FLAG, not a gate. Mirroring that shape here is what makes the
|
|
415
|
+
* live roll and the rebuilt roll the same number: `rollTurn` is handed this
|
|
416
|
+
* exact object on reopen, so an estimated exchange adds the same buckets it
|
|
417
|
+
* added live and is counted under `estimated` in both.
|
|
418
|
+
*
|
|
419
|
+
* Returns `null` when there is neither reported usage nor text to estimate
|
|
420
|
+
* from — the one case that must stay unrecorded, because a fabricated
|
|
421
|
+
* `{input: 0, output: 0}` row would read as a measured zero forever after.
|
|
422
|
+
*
|
|
423
|
+
* @param {object} [usage] the response's `usage` object
|
|
424
|
+
* @param {object} [turn] its accounting (turnAccounting), when already done
|
|
425
|
+
* @param {{model?: string, costUsd?: number, calls?: number, prompt?: string,
|
|
426
|
+
* reply?: string}} [opts]
|
|
427
|
+
* @returns {object|null} the row's ledger fields
|
|
428
|
+
*/
|
|
429
|
+
function ledgerRow(usage, turn, opts = {}) {
|
|
430
|
+
const t = turn || turnAccounting(usage, opts.model, { costUsd: opts.costUsd });
|
|
431
|
+
const calls = Number.isFinite(opts.calls) && opts.calls > 0 ? opts.calls : undefined;
|
|
432
|
+
if (t.tokens != null) {
|
|
433
|
+
const row = { tokens: usageBuckets(usage) };
|
|
434
|
+
// Only a SETTLED charge is persisted. The CLI writes `costUsd` for a real
|
|
435
|
+
// charge only, and the rebuild prices an unpriced row from the same rate
|
|
436
|
+
// table this window used — persisting a local guess would let a stale
|
|
437
|
+
// table outlive the change that wrote it.
|
|
438
|
+
if (t.real && t.cost != null) row.costUsd = t.cost;
|
|
439
|
+
if (calls !== undefined) row.calls = calls;
|
|
440
|
+
return row;
|
|
441
|
+
}
|
|
442
|
+
const est = estimatedBuckets(opts.prompt, opts.reply);
|
|
443
|
+
if (!est) return null;
|
|
444
|
+
const row = { tokens: est };
|
|
445
|
+
if (calls !== undefined) row.calls = calls;
|
|
446
|
+
return row;
|
|
447
|
+
}
|
|
448
|
+
|
|
310
449
|
/**
|
|
311
450
|
* The CLI's rendering of a session tally — `cli/src/format.js fmtTokens`, which
|
|
312
451
|
* is the one `tokenSummary` actually imports (`cli/src/app.js:50`). NOT the
|
|
@@ -343,10 +482,19 @@ function fmtRoll(roll) {
|
|
|
343
482
|
// are kept, because a line that is only *sometimes* shaped like the CLI's is a
|
|
344
483
|
// lookalike rather than the same quantity. The call count is also the number
|
|
345
484
|
// that reveals a fan-out, which is why it is not hidden at 1.
|
|
346
|
-
const bits = [
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
485
|
+
const bits = [];
|
|
486
|
+
// The token half only when something was actually counted. `rollTurn` counts
|
|
487
|
+
// turns and calls BEFORE it looks at the token count, so a dispatch that
|
|
488
|
+
// reported nothing still reaches here — and printing its empty tally would
|
|
489
|
+
// put `0 tok (0 in / 0 out)` on the topbar, a figure the meter never took,
|
|
490
|
+
// which reads as a counter that does not move. The turn count is still
|
|
491
|
+
// stated, because that much is true.
|
|
492
|
+
if (r.tokens > 0) {
|
|
493
|
+
bits.push(
|
|
494
|
+
`${fmtTokens(r.tokens)} tok (${fmtTokens(r.input)} in / ${fmtTokens(r.output)} out)`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
bits.push(`${r.calls} call${r.calls === 1 ? '' : 's'}`);
|
|
350
498
|
// Money is where this line departs from `tokenSummary`, deliberately: that
|
|
351
499
|
// one sums a single `session.cost` in EUR via fmtEur, while this surface keeps
|
|
352
500
|
// a settled charge and a local estimate apart so a `~`-estimate can never be
|
|
@@ -363,6 +511,11 @@ function fmtRoll(roll) {
|
|
|
363
511
|
// the count appears only when something went unreported, and it never changes
|
|
364
512
|
// a number — it only says the number is not the whole story.
|
|
365
513
|
if (r.unknown) bits.push(`${r.unknown} unrpt`);
|
|
514
|
+
// The other half of the same honesty: a total that includes estimated
|
|
515
|
+
// exchanges says so, because the CLI marks the same rows `real: false` and a
|
|
516
|
+
// reader is entitled to know which figure they are looking at. It changes no
|
|
517
|
+
// number — it says the number is partly inferred.
|
|
518
|
+
if (r.estimated) bits.push(`${r.estimated} est`);
|
|
366
519
|
return bits.join(' · ');
|
|
367
520
|
}
|
|
368
521
|
|
|
@@ -387,9 +540,12 @@ if (typeof module !== 'undefined' && module.exports) {
|
|
|
387
540
|
usageBuckets,
|
|
388
541
|
usageCost,
|
|
389
542
|
turnAccounting,
|
|
543
|
+
estimateTokens,
|
|
544
|
+
estimatedBuckets,
|
|
390
545
|
emptyRoll,
|
|
391
546
|
rollTurn,
|
|
392
547
|
rollMessages,
|
|
548
|
+
ledgerRow,
|
|
393
549
|
fmtTokens,
|
|
394
550
|
fmtRoll,
|
|
395
551
|
fmtCost,
|