diff --git a/app/src/features/account/TokensByBot.tsx b/app/src/features/account/TokensByBot.tsx index 7b4d33fa..3ed43b50 100644 --- a/app/src/features/account/TokensByBot.tsx +++ b/app/src/features/account/TokensByBot.tsx @@ -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"; @@ -99,14 +98,7 @@ function Row({ bot, most }: { bot: BotTokens; most: number }) { style={{ width: `${(used / most) * 100}%` }} /> - - {u.detail( - bot.crew, - bot.turns, - tokens(bot.tokens.cacheRead), - reloadedTokens(bot.tokens.reloaded), - )} - + {u.detail(bot.crew, bot.turns)} ); @@ -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 (
- {t.chat.run.done(time, used && tokens(usedTokens(used)), reloaded)} + {t.chat.run.done(time, used && tokens(usedTokens(used)))}
); } diff --git a/app/src/features/chat/chat.test.tsx b/app/src/features/chat/chat.test.tsx index 3a45a272..b443df19 100644 --- a/app/src/features/chat/chat.test.tsx +++ b/app/src/features/chat/chat.test.tsx @@ -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 () => { diff --git a/app/src/features/chat/context.test.tsx b/app/src/features/chat/context.test.tsx index c682a61e..a3407b50 100644 --- a/app/src/features/chat/context.test.tsx +++ b/app/src/features/chat/context.test.tsx @@ -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. @@ -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", () => { diff --git a/app/src/i18n/en/account.ts b/app/src/i18n/en/account.ts index 91ef15c0..c9f48c36 100644 --- a/app/src/i18n/en/account.ts +++ b/app/src/i18n/en/account.ts @@ -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", }, }, diff --git a/app/src/i18n/en/chat.ts b/app/src/i18n/en/chat.ts index caedd348..f1be1055 100644 --- a/app/src/i18n/en/chat.ts +++ b/app/src/i18n/en/chat.ts @@ -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 = { @@ -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}`, }, diff --git a/app/src/i18n/es/account.ts b/app/src/i18n/es/account.ts index 291d8182..8d29564f 100644 --- a/app/src/i18n/es/account.ts +++ b/app/src/i18n/es/account.ts @@ -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", }, }, diff --git a/app/src/i18n/es/chat.ts b/app/src/i18n/es/chat.ts index bc938d6f..bcd7ee66 100644 --- a/app/src/i18n/es/chat.ts +++ b/app/src/i18n/es/chat.ts @@ -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}`, }, diff --git a/app/src/i18n/pt-BR/account.ts b/app/src/i18n/pt-BR/account.ts index 93085ee6..8779cfe6 100644 --- a/app/src/i18n/pt-BR/account.ts +++ b/app/src/i18n/pt-BR/account.ts @@ -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", }, }, diff --git a/app/src/i18n/pt-BR/chat.ts b/app/src/i18n/pt-BR/chat.ts index 2a2f75f0..58bdd341 100644 --- a/app/src/i18n/pt-BR/chat.ts +++ b/app/src/i18n/pt-BR/chat.ts @@ -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}`, }, diff --git a/app/src/lib/format.ts b/app/src/lib/format.ts index 808e4631..4622cc84 100644 --- a/app/src/lib/format.ts +++ b/app/src/lib/format.ts @@ -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". */ diff --git a/crates/botloft-store/src/usage.rs b/crates/botloft-store/src/usage.rs index 190c6fb4..092e9198 100644 --- a/crates/botloft-store/src/usage.rs +++ b/crates/botloft-store/src/usage.rs @@ -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| { diff --git a/docs/spec.md b/docs/spec.md index f87686f7..3c6ef1d4 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -413,9 +413,9 @@ A compactação custa um pedido ao modelo com a conversa inteira (barato com o c O dono vê quantos tokens cada bot gasta: em cada turno, no chat, e somados por bot, no diálogo de uso. Nunca em dinheiro: o `total_cost_usd` do Claude Code é preço de API e soma o processo inteiro (19), e quem usa um plano do Claude não paga isso. - **De onde vem.** O `usage` do `result` é a soma dos pedidos ao modelo daquele turno (19). O item `turn` guarda `tokens`: `input` (`input_tokens`), `cacheWrite` (`cache_creation_input_tokens`), `cacheRead` (`cache_read_input_tokens`) e `output` (`output_tokens`, com o pensamento), e ainda `reloaded`, a conta do daemon descrita abaixo (0 nos itens antigos). O `modelUsage` não serve: ele acumula entre turnos. -- **O que conta.** O número em destaque é `input + cacheWrite - reloaded + output`: o texto novo que o modelo leu e o que escreveu. O Claude Code manda quase tudo que é novo (a mensagem, o resultado de uma ferramenta) para o cache, por isso o `input` sozinho fica perto de zero e o novo está no `cacheWrite`. O `cacheRead` é quase todo a conversa relida a cada pedido, pesa bem menos e numa conversa longa seria a maior parte do total; ele aparece à parte, como "relidos". -- **A conversa recarregada.** Cada pedido manda a conversa inteira. Quando o cache expirou, porque o bot ficou parado ou o processo reiniciou, a parte da conversa que o cache não tinha mais é gravada de novo e vem no `cacheWrite`, misturada com o que é novo; num turno curto depois de uma pausa ela é quase tudo (19). O daemon separa essa parte em `reloaded`, sem guardar nada: para cada pedido da conversa principal (sem subagentes nem respostas sintéticas, contado uma vez por `message.id`), o que a conversa já tinha e não veio do cache, `min(cacheWrite, antes - cacheRead)`, onde `antes` é `input + cacheWrite + cacheRead` do pedido anterior ou, sem pedido visto, o tamanho que o Claude Code respondeu (8.6). Uma conversa nova ou recém-compactada não tem `antes` e não conta nada. O valor é limitado ao `cacheWrite` do `result`. O app mostra à parte só a partir de mil tokens, como "recarregou a conversa (72 mil)" no chat e "recarregados" no diálogo de uso. -- **No chat.** A linha do fim do turno diz a duração e os tokens ("Concluído em 4,2 s · 2,5 mil tokens"), e a conversa recarregada quando houve; o detalhe (novos, escritos, relidos, recarregados) fica no título da linha. A unidade é o turno, não a mensagem: mensagens que chegam juntas entram num turno só, e não dá para dividir os tokens entre elas. +- **O que conta.** O número em destaque é `input + cacheWrite + output`: o que o modelo leu sem vir do cache (o texto novo e a conversa recarregada, abaixo) e o que escreveu. O Claude Code manda quase tudo que é novo (a mensagem, o resultado de uma ferramenta) para o cache, por isso o `input` sozinho fica perto de zero e o novo está no `cacheWrite`. O `cacheRead` é quase todo a conversa relida a cada pedido, pesa bem menos e numa conversa longa seria a maior parte do total. O app não o mostra: um número "relidos" ao lado do gasto confundia quem lê. Ele continua gravado no item `turn` e somado no `usage.tokens`. +- **A conversa recarregada.** Cada pedido manda a conversa inteira. Quando o cache expirou, porque o bot ficou parado ou o processo reiniciou, a parte da conversa que o cache não tinha mais é gravada de novo e vem no `cacheWrite`, misturada com o que é novo; num turno curto depois de uma pausa ela é quase tudo (19). O daemon separa essa parte em `reloaded`, sem guardar nada: para cada pedido da conversa principal (sem subagentes nem respostas sintéticas, contado uma vez por `message.id`), o que a conversa já tinha e não veio do cache, `min(cacheWrite, antes - cacheRead)`, onde `antes` é `input + cacheWrite + cacheRead` do pedido anterior ou, sem pedido visto, o tamanho que o Claude Code respondeu (8.6). Uma conversa nova ou recém-compactada não tem `antes` e não conta nada. O valor é limitado ao `cacheWrite` do `result`. O app não mostra essa parte: ela entra no número do turno, porque é gasto de verdade do plano, e um número a mais ao lado confundia quem lê. Um turno curto depois de uma pausa mostra um número grande, e isso é verdade. O `reloaded` continua gravado no item `turn`. +- **No chat.** A linha do fim do turno diz a duração e os tokens ("Concluído em 4,2 s · 2,5 mil tokens"); o detalhe (lidos e escritos) fica no título da linha. A unidade é o turno, não a mensagem: mensagens que chegam juntas entram num turno só, e não dá para dividir os tokens entre elas. - **Por bot.** `usage.tokens {since}` soma os itens `turn` com `tokens` terminados a partir de `since` (ms Unix), por bot, com nome, cor, o nome da equipe (dois bots podem ter o mesmo nome em equipes diferentes) e se está arquivado, do que mais gastou para o que menos. Bots sem turno no período ficam de fora; bots excluídos levam os itens junto (7.6). O app oferece hoje (desde a meia-noite local), 7 dias, 30 dias e tudo, com o total dos bots embaixo. - Uma compactação (8.6) não vira item `turn` e não entra na conta.