Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 4 additions & 16 deletions app/src/features/account/TokensByBot.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// Tokens each bot used over a period (spec 8.7), in the usage dialog: the
// new text read plus the replies written, with the conversation reread
// each time shown apart. Each bot shows its crew, as two crews can have
// text read plus the replies written. Each bot shows its crew, as two crews can have
// bots with the same name.

import { useEffect, useState } from "react";
import { useT } from "../../i18n";
import { errorText } from "../../lib/api";
import { reloadedTokens, tokens, usedTokens } from "../../lib/format";
import { tokens, usedTokens } from "../../lib/format";
import type { BotTokens } from "../../lib/protocol.gen";
import { useApi } from "../../store/context";
import { Callout } from "../../ui/Callout";
Expand Down Expand Up @@ -99,14 +98,7 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) {
style={{ width: `${(used / most) * 100}%` }}
/>
</div>
<span className="text-muted text-xs">
{u.detail(
bot.crew,
bot.turns,
tokens(bot.tokens.cacheRead),
reloadedTokens(bot.tokens.reloaded),
)}
</span>
<span className="text-muted text-xs">{u.detail(bot.crew, bot.turns)}</span>
</div>
</li>
);
Expand All @@ -116,17 +108,13 @@ function Total({ bots }: { bots: BotTokens[] }) {
const u = useT().account.usage.tokens;
const used = bots.reduce((sum, bot) => sum + usedTokens(bot.tokens), 0);
const turns = bots.reduce((sum, bot) => sum + bot.turns, 0);
const reread = bots.reduce((sum, bot) => sum + bot.tokens.cacheRead, 0);
const reloaded = bots.reduce((sum, bot) => sum + bot.tokens.reloaded, 0);
return (
<li className="flex flex-col gap-1 border-line border-t pt-3">
<div className="flex items-baseline justify-between gap-3 font-semibold">
<span>{u.total}</span>
<span className="tabular-nums">{tokens(used)}</span>
</div>
<span className="text-muted text-xs">
{u.detail(null, turns, tokens(reread), reloadedTokens(reloaded))}
</span>
<span className="text-muted text-xs">{u.detail(null, turns)}</span>
</li>
);
}
16 changes: 8 additions & 8 deletions app/src/features/account/tokens.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ describe("tokens by bot", () => {
const rows = within(list).getAllByRole("listitem");
// Each turn: 12 new, 1,800 into the cache and 640 written count; 24,000 reread do not.
expect(rows.map((row) => row.textContent)).toEqual([
"Scout4.9kOps · Worked 2 times · 48k reread",
"Writer2.5kOps · Worked once · 24k reread",
"All bots7.4kWorked 3 times · 72k reread",
"Scout4.9kOps · Worked 2 times",
"Writer2.5kOps · Worked once",
"All bots7.4kWorked 3 times",
]);

fireEvent.click(within(dialog).getByRole("radio", { name: "7 days" }));
Expand All @@ -45,7 +45,7 @@ describe("tokens by bot", () => {
expect(dialog.textContent).toContain("All bots9.8k");
});

test("the conversation sent again after a pause counts apart", async () => {
test("the conversation sent again after a pause counts in the total", async () => {
const fake = new FakeBotloft();
const ops = fake.addCrew("Ops");
const scout = fake.addBot(ops.id, "Scout", "Finds sources");
Expand All @@ -64,7 +64,7 @@ describe("tokens by bot", () => {
within(list)
.getAllByRole("listitem")
.map((row) => row.textContent),
).toEqual(["Scout4.1kOps · Worked 2 times · 24k reread · 72k reloaded"]);
).toEqual(["Scout76.1kOps · Worked 2 times"]);
});

test("bots with the same name in two crews show their crew", async () => {
Expand All @@ -81,9 +81,9 @@ describe("tokens by bot", () => {
.getAllByRole("listitem")
.map((row) => row.textContent),
).toEqual([
"Scout2.5kNewsroom · Worked once · 24k reread",
"Scout2.5kOps · Worked once · 24k reread",
"All bots4.9kWorked 2 times · 48k reread",
"Scout2.5kNewsroom · Worked once",
"Scout2.5kOps · Worked once",
"All bots4.9kWorked 2 times",
]);
});

Expand Down
9 changes: 3 additions & 6 deletions app/src/features/chat/BotRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { CircleAlert } from "lucide-react";
import { memo, type ReactNode } from "react";
import { useT } from "../../i18n";
import { duration, reloadedTokens, tokens, usedTokens, when } from "../../lib/format";
import { duration, tokens, usedTokens, when } from "../../lib/format";
import type { Bot, ChatItem, TurnItem } from "../../lib/protocol.gen";
import { useArrival } from "../../ui/motion";
import { BotAvatar, moodOf } from "../bots/BotAvatar";
Expand Down Expand Up @@ -33,16 +33,13 @@ function TurnEnd({ turn }: { turn: TurnItem }) {
}
const time = duration(turn.durationMs);
const used = turn.tokens;
const reloaded = used && reloadedTokens(used.reloaded);
const detail = used && {
read: tokens(used.input + used.cacheWrite - used.reloaded),
read: tokens(used.input + used.cacheWrite),
wrote: tokens(used.output),
reread: tokens(used.cacheRead),
reloaded,
};
return (
<p className="text-muted text-xs" title={t.chat.run.took(time, detail)}>
{t.chat.run.done(time, used && tokens(usedTokens(used)), reloaded)}
{t.chat.run.done(time, used && tokens(usedTokens(used)))}
</p>
);
}
Expand Down
4 changes: 1 addition & 3 deletions app/src/features/chat/chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,7 @@ describe("chat", () => {
expect(within(chat()).getByText("Command")).toBeDefined();
expect(within(chat()).getByText("npm test")).toBeDefined();
const done = within(chat()).getByText("Done in 4.2 s · 2.5k tokens");
expect(done.title).toBe(
"Took 4.2 s. Read 1.8k new tokens and wrote 640; reread 24k of the conversation, which weighs much less.",
);
expect(done.title).toBe("Took 4.2 s. Read 1.8k tokens and wrote 640.");
});

test("an approval waits for the owner, and a denial carries a note", async () => {
Expand Down
22 changes: 3 additions & 19 deletions app/src/features/chat/context.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function open() {
}

describe("conversation space", () => {
test("a turn after a pause tells the conversation it sent again apart", async () => {
test("a turn after a pause counts the conversation it sent again", async () => {
const { fake, scout } = await openScout();
act(() => {
// The turn seen in spec 19: almost all of it was the conversation again.
Expand All @@ -51,24 +51,8 @@ describe("conversation space", () => {
output: 983,
});
});
const done = screen.getByText("Done in 4.2 s · 1.6k tokens · reloaded the conversation (72k)");
expect(done.title).toBe(
"Took 4.2 s. Read 638 new tokens and wrote 983; reread 0 of the conversation, which weighs much less. It also sent 72k of the conversation again, as the model no longer had it after a pause.",
);
});

test("a little sent again is not worth a mention", async () => {
const { fake, scout } = await openScout();
act(() => {
fake.chat.turn(scout.id, null, {
input: 2,
cacheWrite: 1_400,
reloaded: 300,
cacheRead: 40_000,
output: 100,
});
});
expect(screen.getByText("Done in 4.2 s · 1.2k tokens")).toBeDefined();
const done = screen.getByText("Done in 4.2 s · 73.6k tokens");
expect(done.title).toBe("Took 4.2 s. Read 72.6k tokens and wrote 983.");
});

test("token counts read short", () => {
Expand Down
8 changes: 3 additions & 5 deletions app/src/i18n/en/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ export const account = {
tokens: {
title: "Tokens by bot",
intro:
"Tokens are the pieces of text a bot reads and writes. The count adds the new text read and the replies written. The conversation reread each time shows apart, as it weighs much less, and so does the conversation sent again after a pause.",
"Tokens are the pieces of text a bot reads and writes. The count adds the text read and the replies written.",
period: "Period",
periods: { today: "Today", week: "7 days", month: "30 days", all: "All time" },
empty: "No bot worked in this period.",
failed: "Could not load the tokens",
archived: "archived",
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Worked ${times === 1 ? "once" : `${times} times`} · ${reread} reread${
reloaded === null ? "" : ` · ${reloaded} reloaded`
}`,
detail: (crew: string | null, times: number) =>
`${crew === null ? "" : `${crew} · `}Worked ${times === 1 ? "once" : `${times} times`}`,
total: "All bots",
},
},
Expand Down
17 changes: 3 additions & 14 deletions app/src/i18n/en/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
export interface TurnTokens {
read: string;
wrote: string;
reread: string;
/** The conversation sent again after a pause, if it was worth telling. */
reloaded: string | null;
}

export const chat = {
Expand Down Expand Up @@ -56,19 +53,11 @@ export const chat = {
},
run: {
working: "Working",
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Done in ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · reloaded the conversation (${reloaded})`
}`,
done: (time: string, tokens: string | null) =>
`Done in ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
took: (time: string, tokens: TurnTokens | null) =>
`Took ${time}${
tokens === null
? ""
: `. Read ${tokens.read} new tokens and wrote ${tokens.wrote}; reread ${tokens.reread} of the conversation, which weighs much less.${
tokens.reloaded === null
? ""
: ` It also sent ${tokens.reloaded} of the conversation again, as the model no longer had it after a pause.`
}`
tokens === null ? "" : `. Read ${tokens.read} tokens and wrote ${tokens.wrote}.`
}`,
stopped: (reason: string) => `The bot stopped working on this: ${reason}`,
},
Expand Down
8 changes: 3 additions & 5 deletions app/src/i18n/es/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto nuevo leído y las respuestas escritas. La conversación releída cada vez aparece aparte, porque pesa mucho menos, y también la conversación enviada de nuevo tras una pausa.",
"Los tokens son los trozos de texto que un bot lee y escribe. La cuenta suma el texto leído y las respuestas escritas.",
period: "Período",
periods: { today: "Hoy", week: "7 días", month: "30 días", all: "Todo" },
empty: "Ningún bot trabajó en este período.",
failed: "No se pudieron cargar los tokens",
archived: "archivado",
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Trabajó ${times === 1 ? "1 vez" : `${times} veces`} · ${reread} releídos${
reloaded === null ? "" : ` · ${reloaded} recargados`
}`,
detail: (crew: string | null, times: number) =>
`${crew === null ? "" : `${crew} · `}Trabajó ${times === 1 ? "1 vez" : `${times} veces`}`,
total: "Todos los bots",
},
},
Expand Down
14 changes: 3 additions & 11 deletions app/src/i18n/es/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,11 @@ export const chat: Messages["chat"] = {
},
run: {
working: "Trabajando",
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Terminado en ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · recargó la conversación (${reloaded})`
}`,
done: (time: string, tokens: string | null) =>
`Terminado en ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
took: (time: string, tokens: TurnTokens | null) =>
`Tardó ${time}${
tokens === null
? ""
: `. Leyó ${tokens.read} tokens nuevos y escribió ${tokens.wrote}; releyó ${tokens.reread} de la conversación, que pesa mucho menos.${
tokens.reloaded === null
? ""
: ` También volvió a enviar ${tokens.reloaded} de la conversación, que el modelo ya no tenía tras una pausa.`
}`
tokens === null ? "" : `. Leyó ${tokens.read} tokens y escribió ${tokens.wrote}.`
}`,
stopped: (reason: string) => `El bot dejó de trabajar en esto: ${reason}`,
},
Expand Down
8 changes: 3 additions & 5 deletions app/src/i18n/pt-BR/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,15 @@ export const account: Messages["account"] = {
tokens: {
title: "Tokens por bot",
intro:
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto novo lido e as respostas escritas. A conversa relida a cada vez aparece à parte, porque pesa bem menos, e a conversa mandada de novo depois de uma pausa também.",
"Tokens são os pedaços de texto que um bot lê e escreve. A conta soma o texto lido e as respostas escritas.",
period: "Período",
periods: { today: "Hoje", week: "7 dias", month: "30 dias", all: "Tudo" },
empty: "Nenhum bot trabalhou nesse período.",
failed: "Não deu para carregar os tokens",
archived: "arquivado",
/** `crew` is null on the total of all bots. */
detail: (crew: string | null, times: number, reread: string, reloaded: string | null) =>
`${crew === null ? "" : `${crew} · `}Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`} · ${reread} relidos${
reloaded === null ? "" : ` · ${reloaded} recarregados`
}`,
detail: (crew: string | null, times: number) =>
`${crew === null ? "" : `${crew} · `}Trabalhou ${times === 1 ? "1 vez" : `${times} vezes`}`,
total: "Todos os bots",
},
},
Expand Down
14 changes: 3 additions & 11 deletions app/src/i18n/pt-BR/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,11 @@ export const chat: Messages["chat"] = {
},
run: {
working: "Trabalhando",
done: (time: string, tokens: string | null, reloaded: string | null) =>
`Concluído em ${time}${tokens === null ? "" : ` · ${tokens} tokens`}${
reloaded === null ? "" : ` · recarregou a conversa (${reloaded})`
}`,
done: (time: string, tokens: string | null) =>
`Concluído em ${time}${tokens === null ? "" : ` · ${tokens} tokens`}`,
took: (time: string, tokens: TurnTokens | null) =>
`Levou ${time}${
tokens === null
? ""
: `. Leu ${tokens.read} tokens novos e escreveu ${tokens.wrote}; releu ${tokens.reread} da conversa, que pesa bem menos.${
tokens.reloaded === null
? ""
: ` Também mandou de novo ${tokens.reloaded} da conversa, que o modelo já não tinha depois de uma pausa.`
}`
tokens === null ? "" : `. Leu ${tokens.read} tokens e escreveu ${tokens.wrote}.`
}`,
stopped: (reason: string) => `O bot parou de trabalhar nisto: ${reason}`,
},
Expand Down
17 changes: 4 additions & 13 deletions app/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,12 @@ export function tokens(count: number): string {
}

/**
* The tokens a turn counts for (spec 8.7): new input read, with what went
* into the prompt cache, and what the model wrote. The conversation reread
* from the cache weighs much less and is shown apart, and so is the
* conversation written to the cache again after it expired.
* The tokens a turn counts for (spec 8.7): input read, with what went into
* the prompt cache (the conversation sent again after a pause too), and
* what the model wrote. The conversation reread from the cache is left out.
*/
export function usedTokens(usage: TokenUsage): number {
return usage.input + usage.cacheWrite - usage.reloaded + usage.output;
}

/** Below this, the conversation sent again is not worth a mention. */
const RELOAD_SHOWN = 1000;

/** The conversation sent again, written as a number, or `null` if it is too little to tell. */
export function reloadedTokens(count: number): string | null {
return count >= RELOAD_SHOWN ? tokens(count) : null;
return usage.input + usage.cacheWrite + usage.output;
}

/** "0.8 s", "42 s", "3 min 5 s". */
Expand Down
2 changes: 1 addition & 1 deletion crates/botloft-store/src/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Store {
AND json_type(c.data, '$.tokens') = 'object' \
GROUP BY b.id \
ORDER BY SUM(json_extract(c.data, '$.tokens.input')) \
+ SUM(json_extract(c.data, '$.tokens.cacheWrite')) - reloaded + output DESC, \
+ SUM(json_extract(c.data, '$.tokens.cacheWrite')) + output DESC, \
b.name, w.name",
)?;
let rows = stmt.query_map(params![since], |row| {
Expand Down
Loading
Loading