crewforth 3.5.0-rc.1 → 3.5.0-rc.2
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/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/kit/CLAUDE.md +1 -1
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/smoke-test.sh +60 -2
- package/kit/hooks/prompt-approval.sh +5 -5
- package/kit/skills/crew-plan/SKILL.md +2 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/spec-planning/SKILL.md +9 -2
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +21 -18
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +33 -29
- package/kit/studio/web/map.js +74 -73
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from './timeline-plan.js';
|
|
8
8
|
import { Press } from './press.js';
|
|
9
9
|
import { shownType, hoverOf, modelOf, modelFamily } from './names.js';
|
|
10
|
+
import { t, numLocale } from './i18n.js';
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
function mk(tag, cls, text) {
|
|
@@ -56,7 +57,7 @@ export class Timeline {
|
|
|
56
57
|
#build() {
|
|
57
58
|
this.root.classList.add('tl');
|
|
58
59
|
this.axis = mk('div', 'tl-axis');
|
|
59
|
-
this.axisLabel = mk('div', 'tl-axis-label', 'Agent');
|
|
60
|
+
this.axisLabel = mk('div', 'tl-axis-label', t('Agent'));
|
|
60
61
|
this.axisTrack = mk('div', 'tl-axis-track');
|
|
61
62
|
this.axis.append(this.axisLabel, this.axisTrack);
|
|
62
63
|
|
|
@@ -69,7 +70,7 @@ export class Timeline {
|
|
|
69
70
|
this.noteEl = mk('div', 'tl-note');
|
|
70
71
|
this.drawer = mk('div', 'tl-drawer');
|
|
71
72
|
this.drawer.setAttribute('role', 'region');
|
|
72
|
-
this.drawer.setAttribute('aria-label', 'Selected agent');
|
|
73
|
+
this.drawer.setAttribute('aria-label', t('Selected agent'));
|
|
73
74
|
this.drawer.hidden = true;
|
|
74
75
|
this.empty = mk('div', 'tl-empty');
|
|
75
76
|
this.empty.hidden = true;
|
|
@@ -77,7 +78,7 @@ export class Timeline {
|
|
|
77
78
|
// no word would read as a session in which nothing happened.
|
|
78
79
|
this.away = mk('div', 'tl-away');
|
|
79
80
|
this.away.hidden = true;
|
|
80
|
-
const whole = mk('button', 'btn sm', 'Show the whole session');
|
|
81
|
+
const whole = mk('button', 'btn sm', t('Show the whole session'));
|
|
81
82
|
whole.type = 'button';
|
|
82
83
|
whole.addEventListener('click', () => { this.range = 'all'; this.render(); this.#changed(); });
|
|
83
84
|
this.awayText = mk('span', null);
|
|
@@ -205,8 +206,8 @@ export class Timeline {
|
|
|
205
206
|
this.body.hidden = agents.length === 0;
|
|
206
207
|
this.axis.hidden = agents.length === 0;
|
|
207
208
|
if (!agents.length) {
|
|
208
|
-
this.empty.replaceChildren(mk('strong', null, 'No agents yet'),
|
|
209
|
-
mk('span', null, 'This session has not delegated to a subagent. When it does, each one gets a row here.'));
|
|
209
|
+
this.empty.replaceChildren(mk('strong', null, t('No agents yet')),
|
|
210
|
+
mk('span', null, t('This session has not delegated to a subagent. When it does, each one gets a row here.')));
|
|
210
211
|
this.noteEl.hidden = true;
|
|
211
212
|
this.nowLine.hidden = true;
|
|
212
213
|
this.away.hidden = true;
|
|
@@ -243,7 +244,7 @@ export class Timeline {
|
|
|
243
244
|
this.nowLine.hidden = !this.live || !win || nowX < 0 || nowX > width + 1;
|
|
244
245
|
if (!this.nowLine.hidden) {
|
|
245
246
|
this.nowLine.style.left = `${this.labelWidth() + Math.min(nowX, width - 1)}px`;
|
|
246
|
-
this.nowLine.dataset.label =
|
|
247
|
+
this.nowLine.dataset.label = `${t('now')} ${clock(now)}`;
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
// Bars exist, and none of them is in the stretch on screen.
|
|
@@ -253,8 +254,8 @@ export class Timeline {
|
|
|
253
254
|
if (!this.away.hidden) {
|
|
254
255
|
const last = Math.max(...timed.map((n) => n.endedAt ?? n.updatedAt ?? n.startedAt));
|
|
255
256
|
this.awayText.textContent = last < win.from
|
|
256
|
-
?
|
|
257
|
-
: 'Nothing in this stretch of time.';
|
|
257
|
+
? t('Nothing in this stretch of time. The last activity ended at {time}.', { time: clock(last) })
|
|
258
|
+
: t('Nothing in this stretch of time.');
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
const note = waitNote(this.owned);
|
|
@@ -281,7 +282,7 @@ export class Timeline {
|
|
|
281
282
|
b.style.left = `${at.left}px`;
|
|
282
283
|
b.style.width = `${at.width}px`;
|
|
283
284
|
const node = this.nodes.find((n) => n.id === seg.id);
|
|
284
|
-
b.setAttribute('aria-label',
|
|
285
|
+
b.setAttribute('aria-label', t('{who}, {state}, {from} to {to}', { who: node ? shownType(node) : t('agent'), state: seg.tone === 'wait' ? t('waiting for you') : (node?.status ?? ''), from: clock(seg.from), to: clock(seg.to) }));
|
|
285
286
|
if (node && this.dimmed(node)) b.classList.add('dim');
|
|
286
287
|
if (seg.id === this.selected) b.classList.add('on');
|
|
287
288
|
b.addEventListener('click', (e) => { e.stopPropagation(); this.#choose(seg.id); });
|
|
@@ -300,7 +301,7 @@ export class Timeline {
|
|
|
300
301
|
#row(r, win, width) {
|
|
301
302
|
if (r.kind === 'more') {
|
|
302
303
|
const row = mk('div', 'tl-row tl-more');
|
|
303
|
-
const b = mk('button', 'tl-more-btn',
|
|
304
|
+
const b = mk('button', 'tl-more-btn', this.group === 'run' ? t('Show {n} more in this run', { n: r.count }) : t('Show {n} more in this group', { n: r.count }));
|
|
304
305
|
b.type = 'button';
|
|
305
306
|
b.addEventListener('click', () => { this.expanded.add(r.group); this.render(); });
|
|
306
307
|
const lab = mk('div', 'tl-lab');
|
|
@@ -353,7 +354,7 @@ export class Timeline {
|
|
|
353
354
|
row.addEventListener('click', () => this.#choose(r.id));
|
|
354
355
|
if (r.timed) this.#bars(track, [r.bars], win, width);
|
|
355
356
|
// No start was recorded, so there is nowhere to put a bar; the row says that instead of drawing one at zero.
|
|
356
|
-
else track.append(mk('span', 'tl-untimed', 'no start time recorded'));
|
|
357
|
+
else track.append(mk('span', 'tl-untimed', t('no start time recorded')));
|
|
357
358
|
if (r.id === this.selected) row.classList.add('on');
|
|
358
359
|
if (this.dimmed(r.node)) row.classList.add('dim');
|
|
359
360
|
}
|
|
@@ -382,7 +383,7 @@ export class Timeline {
|
|
|
382
383
|
const pill = mk('span', 'pill');
|
|
383
384
|
const dot = mk('span', 'dot');
|
|
384
385
|
dot.dataset.tone = st.tone ?? 'none';
|
|
385
|
-
pill.append(dot, mk('span', null, st.word ?? 'State not measured'));
|
|
386
|
+
pill.append(dot, mk('span', null, st.word ?? t('State not measured')));
|
|
386
387
|
const dname = mk('span', 'tl-d-name', shownType(n));
|
|
387
388
|
dname.title = n.agentType ?? '';
|
|
388
389
|
head.append(dname, pill);
|
|
@@ -391,36 +392,36 @@ export class Timeline {
|
|
|
391
392
|
left.append(head, dtask, mk('div', 'sub tl-d-facts', factsOf(n, now, this.hooks.fmt)));
|
|
392
393
|
|
|
393
394
|
const mid = mk('div', 'tl-d-error');
|
|
394
|
-
mid.append(mk('div', 'isec-h', 'Last error'));
|
|
395
|
+
mid.append(mk('div', 'isec-h', t('Last error')));
|
|
395
396
|
const detail = this.hooks.detailOf?.(n);
|
|
396
397
|
if (!detail || detail.loading) {
|
|
397
|
-
mid.append(mk('div', 'ihint', 'Reading the agent transcript…'));
|
|
398
|
+
mid.append(mk('div', 'ihint', t('Reading the agent transcript…')));
|
|
398
399
|
} else if (detail.measured === false) {
|
|
399
400
|
const hint = mk('div', 'ihint');
|
|
400
|
-
hint.append(mk('strong', null, 'Not measured'), mk('div', null, detail.reason || 'no reason given'));
|
|
401
|
+
hint.append(mk('strong', null, t('Not measured')), mk('div', null, detail.reason || t('no reason given')));
|
|
401
402
|
mid.append(hint);
|
|
402
403
|
} else if (!detail.lastError) {
|
|
403
|
-
mid.append(mk('div', 'ihint', 'None — no tool call in this agent\'s transcript came back as an error.'));
|
|
404
|
+
mid.append(mk('div', 'ihint', t('None — no tool call in this agent\'s transcript came back as an error.')));
|
|
404
405
|
} else {
|
|
405
406
|
const e = detail.lastError;
|
|
406
|
-
const text = mk('pre', 'tl-d-errtext', e.truncated ?
|
|
407
|
+
const text = mk('pre', 'tl-d-errtext', e.truncated ? `${t('… {n} earlier characters not shown', { n: (e.length - e.text.length).toLocaleString(numLocale()) })}\n${e.text}` : (e.text || t('(no output)')));
|
|
407
408
|
const count = detail.errors ?? 0;
|
|
408
|
-
const facts = [
|
|
409
|
-
if (e.at != null) facts.push(
|
|
409
|
+
const facts = [count === 1 ? t('{n} error', { n: 1 }) : t('{n} errors', { n: count })];
|
|
410
|
+
if (e.at != null) facts.push(t('last at {time}', { time: clock(e.at, true) }));
|
|
410
411
|
if (e.tool) facts.push(e.tool);
|
|
411
412
|
mid.append(text, mk('div', 'sub', facts.join(' · ')));
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
const acts = mk('div', 'tl-d-acts');
|
|
415
|
-
const show = mk('button', 'btn sm primary', 'Show on graph');
|
|
416
|
+
const show = mk('button', 'btn sm primary', t('Show on graph'));
|
|
416
417
|
show.type = 'button';
|
|
417
418
|
show.addEventListener('click', () => this.hooks.onShowOnGraph?.(n));
|
|
418
|
-
const open = mk('button', 'btn sm', 'Open conversation');
|
|
419
|
+
const open = mk('button', 'btn sm', t('Open conversation'));
|
|
419
420
|
open.type = 'button';
|
|
420
421
|
open.addEventListener('click', () => this.hooks.onOpenConversation?.());
|
|
421
422
|
const close = mk('button', 'btn icon sm ghost');
|
|
422
423
|
close.type = 'button';
|
|
423
|
-
close.setAttribute('aria-label', 'Close');
|
|
424
|
+
close.setAttribute('aria-label', t('Close'));
|
|
424
425
|
close.innerHTML = '<svg class="ic" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8"/></svg>';
|
|
425
426
|
close.addEventListener('click', () => this.#choose(n.id));
|
|
426
427
|
acts.append(show, open, close);
|
package/kit/studio/web/usage.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// a dash, not a smaller number.
|
|
7
7
|
|
|
8
8
|
import { modelName } from './names.js';
|
|
9
|
+
import { t, numLocale } from './i18n.js';
|
|
9
10
|
|
|
10
|
-
export const COST_NOTE = 'Estimated at API list price; on a subscription it is not what you are billed';
|
|
11
|
+
export const COST_NOTE = t('Estimated at API list price; on a subscription it is not what you are billed');
|
|
11
12
|
export const NO_COST = '—';
|
|
12
13
|
|
|
13
14
|
/** 950 · 12.4k · 3.2M · 1.53B */
|
|
@@ -23,7 +24,7 @@ export function fmtCount(n) {
|
|
|
23
24
|
export function fmtCost(usd) {
|
|
24
25
|
if (usd == null || !Number.isFinite(usd)) return NO_COST;
|
|
25
26
|
if (usd > 0 && usd < 0.01) return '<$0.01';
|
|
26
|
-
if (usd >= 1000) return `~$${Math.round(usd).toLocaleString(
|
|
27
|
+
if (usd >= 1000) return `~$${Math.round(usd).toLocaleString(numLocale())}`;
|
|
27
28
|
return `~$${usd.toFixed(2)}`;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -31,12 +32,12 @@ export function fmtCost(usd) {
|
|
|
31
32
|
export function fmtSpan(ms) {
|
|
32
33
|
if (ms == null || !Number.isFinite(ms) || ms < 0) return null;
|
|
33
34
|
const s = Math.round(ms / 1000);
|
|
34
|
-
if (s < 60) return
|
|
35
|
+
if (s < 60) return t('{n}s', { n: s });
|
|
35
36
|
const m = Math.floor(s / 60);
|
|
36
|
-
if (m < 60) return
|
|
37
|
+
if (m < 60) return t('{n}m', { n: m });
|
|
37
38
|
const h = Math.floor(m / 60);
|
|
38
|
-
if (h < 24) return
|
|
39
|
-
return
|
|
39
|
+
if (h < 24) return t('{h}h {m}m', { h, m: m % 60 });
|
|
40
|
+
return t('{d}d {h}h', { d: Math.floor(h / 24), h: h % 24 });
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -45,23 +46,23 @@ export function fmtSpan(ms) {
|
|
|
45
46
|
* @returns { ms, kind: 'worked' | 'elapsed', note } or null
|
|
46
47
|
*/
|
|
47
48
|
export function timeOf(u) {
|
|
48
|
-
if (u?.workedMs) return { ms: u.workedMs, kind: 'worked', note: 'The turns Claude Code timed, added up; a turn still running is not in it yet' };
|
|
49
|
-
if (u?.durationMs != null) return { ms: u.durationMs, kind: 'elapsed', note: 'From the first record to the last, pauses included: no turn was timed' };
|
|
49
|
+
if (u?.workedMs) return { ms: u.workedMs, kind: 'worked', note: t('The turns Claude Code timed, added up; a turn still running is not in it yet') };
|
|
50
|
+
if (u?.durationMs != null) return { ms: u.durationMs, kind: 'elapsed', note: t('From the first record to the last, pauses included: no turn was timed') };
|
|
50
51
|
return null;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** Why there is no estimate, when there is none. */
|
|
54
55
|
export function costNote(u) {
|
|
55
56
|
if (!u?.unpriced?.length) return COST_NOTE;
|
|
56
|
-
return
|
|
57
|
+
return t('No estimate: the price table has no row for {models}', { models: u.unpriced.join(', ') });
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/** "40s", "4m 40s", "3h 5m": a span that is being counted, so the seconds show while they matter. */
|
|
60
61
|
export function fmtRunning(ms) {
|
|
61
62
|
if (ms == null || !Number.isFinite(ms) || ms < 0) return null;
|
|
62
63
|
const s = Math.floor(ms / 1000);
|
|
63
|
-
if (s < 60) return
|
|
64
|
-
if (s < 3600) return
|
|
64
|
+
if (s < 60) return t('{n}s', { n: s });
|
|
65
|
+
if (s < 3600) return t('{m}m {s}s', { m: Math.floor(s / 60), s: s % 60 });
|
|
65
66
|
return fmtSpan(ms);
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -71,15 +72,15 @@ export function fmtRunning(ms) {
|
|
|
71
72
|
*/
|
|
72
73
|
export function usageLine(u, { live = false, now = null } = {}) {
|
|
73
74
|
if (!u) return null;
|
|
74
|
-
const
|
|
75
|
-
const span = live && u.startedAt != null && now != null ? fmtSpan(now - u.startedAt) : (
|
|
75
|
+
const took = timeOf(u);
|
|
76
|
+
const span = live && u.startedAt != null && now != null ? fmtSpan(now - u.startedAt) : (took ? fmtSpan(took.ms) : null);
|
|
76
77
|
const tokens = typeof u.tokens === 'number' ? u.tokens : u.tokens?.fresh;
|
|
77
78
|
return [span, tokens ? fmtCount(tokens) : null, tokens ? fmtCost(u.cost) : null].filter(Boolean).join(' · ') || null;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const kinds = (k, cached = true) => [
|
|
81
|
-
['Input', fmtCount(k?.input ?? 0)], ['Output', fmtCount(k?.output ?? 0)], ['Cache write', fmtCount(k?.cacheWrite ?? 0)],
|
|
82
|
-
...(cached ? [['Cache read', fmtCount(k?.cacheRead ?? 0)]] : []),
|
|
82
|
+
[t('Input'), fmtCount(k?.input ?? 0)], [t('Output'), fmtCount(k?.output ?? 0)], [t('Cache write'), fmtCount(k?.cacheWrite ?? 0)],
|
|
83
|
+
...(cached ? [[t('Cache read'), fmtCount(k?.cacheRead ?? 0)]] : []),
|
|
83
84
|
];
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -91,40 +92,43 @@ const kinds = (k, cached = true) => [
|
|
|
91
92
|
* read as millions.
|
|
92
93
|
*
|
|
93
94
|
* @param opts.live the session is running: its time is counted from its first record to `opts.now`
|
|
95
|
+
* @param opts.agentRows [name, value] pairs, one per kind of agent (agent-spend.js): what the Agents row opens on
|
|
96
|
+
* instead of the token kinds, as the Cost row opens on its models. Without any, the kinds.
|
|
94
97
|
*/
|
|
95
|
-
export function summaryRows(u, { live = false, now = null } = {}) {
|
|
98
|
+
export function summaryRows(u, { live = false, now = null, agentRows = [] } = {}) {
|
|
96
99
|
if (!u) return [];
|
|
97
100
|
const k = u.tokens ?? {};
|
|
98
101
|
const rows = [];
|
|
99
|
-
const
|
|
102
|
+
const took = timeOf(u);
|
|
100
103
|
if (live && u.startedAt != null && now != null) {
|
|
101
|
-
rows.push({ label: 'Running for', value: fmtRunning(now - u.startedAt), note: 'From the session\'s first record to now' });
|
|
102
|
-
} else if (
|
|
103
|
-
rows.push({ label:
|
|
104
|
-
if (
|
|
104
|
+
rows.push({ label: t('Running for'), value: fmtRunning(now - u.startedAt), note: t('From the session\'s first record to now') });
|
|
105
|
+
} else if (took) {
|
|
106
|
+
rows.push({ label: took.kind === 'worked' ? t('Working time') : t('First to last record'), value: fmtSpan(took.ms), note: took.note });
|
|
107
|
+
if (took.kind === 'worked' && u.durationMs != null) rows.push({ label: t('First to last record'), value: fmtSpan(u.durationMs), note: t('Pauses included; the agents\' transcripts too') });
|
|
105
108
|
}
|
|
106
|
-
rows.push({ label: 'Tokens', value: fmtCount(k.fresh ?? 0), note: 'New tokens: input, output and cache writes. Agents included, each response counted once', detail: kinds(k, false) });
|
|
107
|
-
rows.push({ label: 'From cache', value: fmtCount(k.cacheRead ?? 0), note: 'Read back from the prompt cache: the same context, counted on every response that reused it' });
|
|
108
|
-
const part = (label, p, note) => {
|
|
109
|
+
rows.push({ label: t('Tokens'), value: fmtCount(k.fresh ?? 0), note: t('New tokens: input, output and cache writes. Agents included, each response counted once'), detail: kinds(k, false) });
|
|
110
|
+
rows.push({ label: t('From cache'), value: fmtCount(k.cacheRead ?? 0), note: t('Read back from the prompt cache: the same context, counted on every response that reused it') });
|
|
111
|
+
const part = (label, p, note, detail = null) => {
|
|
109
112
|
if (!p?.tokens?.total) return;
|
|
110
|
-
rows.push({ label, value: `${fmtCount(p.tokens.fresh)} · ${fmtCost(p.cost)}`, note: `${
|
|
113
|
+
rows.push({ label, value: `${fmtCount(p.tokens.fresh)} · ${fmtCost(p.cost)}`, note: `${t('{what}: new tokens and estimated cost', { what: note })}
|
|
114
|
+
${costNote(p)}`, detail: detail ?? kinds(p.tokens) });
|
|
111
115
|
};
|
|
112
|
-
part('Session', u.parts?.session, 'The session\'s own transcript');
|
|
113
|
-
part('Agents', u.parts?.agents, 'Everything its agents did');
|
|
116
|
+
part(t('Session'), u.parts?.session, t('The session\'s own transcript'));
|
|
117
|
+
part(t('Agents'), u.parts?.agents, t('Everything its agents did'), agentRows.length ? agentRows : null);
|
|
114
118
|
// The advisor: a second model the main one consulted mid-response. Its tokens are in the rows above already; this
|
|
115
119
|
// says how much of them it was. No call recorded, no row: a session that never consulted it shows no zero.
|
|
116
120
|
if (u.advisor?.calls) {
|
|
117
121
|
const a = u.advisor;
|
|
118
122
|
const who = Object.keys(u.advisorModels ?? {}).map(modelName).join(', ');
|
|
119
123
|
rows.push({
|
|
120
|
-
label: 'Advisor',
|
|
121
|
-
value: `${a.calls
|
|
122
|
-
note:
|
|
124
|
+
label: t('Advisor'),
|
|
125
|
+
value: `${a.calls === 1 ? t('{n} call', { n: 1 }) : t('{n} calls', { n: a.calls })} \u00b7 ${fmtCount(a.tokens.fresh)} \u00b7 ${fmtCost(a.cost)}`,
|
|
126
|
+
note: `${who ? t('Advice the main model asked a second model for ({who}), priced at that model\'s rates. Included in the totals above; the API\'s own token count leaves it out', { who }) : t('Advice the main model asked a second model for, priced at that model\'s rates. Included in the totals above; the API\'s own token count leaves it out')}\n${costNote(a)}`,
|
|
123
127
|
detail: kinds(a.tokens),
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
// Behind the cost: each model's new tokens and what they come to. A model with no price says so with a dash.
|
|
127
|
-
const perModel = Object.entries(u.byModel ?? {}).map(([model,
|
|
128
|
-
rows.push({ label: 'Cost', value: k.total ? fmtCost(u.cost) : NO_COST, note: costNote(u), detail: perModel.length ? perModel : undefined });
|
|
131
|
+
const perModel = Object.entries(u.byModel ?? {}).map(([model, m]) => [modelName(model), `${fmtCount(m.tokens.fresh)} \u00b7 ${fmtCost(m.cost)}`]);
|
|
132
|
+
rows.push({ label: t('Cost'), value: k.total ? fmtCost(u.cost) : NO_COST, note: costNote(u), detail: perModel.length ? perModel : undefined });
|
|
129
133
|
return rows;
|
|
130
134
|
}
|
package/package.json
CHANGED