Usage: add cost summaries to /usage + mac menu
This commit is contained in:
@@ -59,14 +59,14 @@ describe("commands registry args", () => {
|
||||
name: "mode",
|
||||
description: "mode",
|
||||
type: "string",
|
||||
choices: ["off", "tokens", "full"],
|
||||
choices: ["off", "tokens", "full", "cost"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never });
|
||||
expect(menu?.arg.name).toBe("mode");
|
||||
expect(menu?.choices).toEqual(["off", "tokens", "full"]);
|
||||
expect(menu?.choices).toEqual(["off", "tokens", "full", "cost"]);
|
||||
});
|
||||
|
||||
it("does not show menus when arg already provided", () => {
|
||||
@@ -82,7 +82,7 @@ describe("commands registry args", () => {
|
||||
name: "mode",
|
||||
description: "mode",
|
||||
type: "string",
|
||||
choices: ["off", "tokens", "full"],
|
||||
choices: ["off", "tokens", "full", "cost"],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -141,7 +141,7 @@ describe("commands registry args", () => {
|
||||
name: "mode",
|
||||
description: "on or off",
|
||||
type: "string",
|
||||
choices: ["off", "tokens", "full"],
|
||||
choices: ["off", "tokens", "full", "cost"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -233,14 +233,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||
defineChatCommand({
|
||||
key: "usage",
|
||||
nativeName: "usage",
|
||||
description: "Toggle per-response usage line.",
|
||||
description: "Usage footer or cost summary.",
|
||||
textAlias: "/usage",
|
||||
args: [
|
||||
{
|
||||
name: "mode",
|
||||
description: "off, tokens, or full",
|
||||
description: "off, tokens, full, or cost",
|
||||
type: "string",
|
||||
choices: ["off", "tokens", "full"],
|
||||
choices: ["off", "tokens", "full", "cost"],
|
||||
},
|
||||
],
|
||||
argsMenu: "auto",
|
||||
|
||||
@@ -7,6 +7,8 @@ import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../inf
|
||||
import { parseActivationCommand } from "../group-activation.js";
|
||||
import { parseSendPolicyCommand } from "../send-policy.js";
|
||||
import { normalizeUsageDisplay, resolveResponseUsageMode } from "../thinking.js";
|
||||
import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js";
|
||||
import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
|
||||
import {
|
||||
formatAbortReplyText,
|
||||
isAbortTrigger,
|
||||
@@ -141,10 +143,48 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman
|
||||
|
||||
const rawArgs = normalized === "/usage" ? "" : normalized.slice("/usage".length).trim();
|
||||
const requested = rawArgs ? normalizeUsageDisplay(rawArgs) : undefined;
|
||||
if (rawArgs.toLowerCase().startsWith("cost")) {
|
||||
const sessionSummary = await loadSessionCostSummary({
|
||||
sessionId: params.sessionEntry?.sessionId,
|
||||
sessionEntry: params.sessionEntry,
|
||||
sessionFile: params.sessionEntry?.sessionFile,
|
||||
config: params.cfg,
|
||||
});
|
||||
const summary = await loadCostUsageSummary({ days: 30, config: params.cfg });
|
||||
|
||||
const sessionCost = formatUsd(sessionSummary?.totalCost);
|
||||
const sessionTokens = sessionSummary?.totalTokens
|
||||
? formatTokenCount(sessionSummary.totalTokens)
|
||||
: undefined;
|
||||
const sessionMissing = sessionSummary?.missingCostEntries ?? 0;
|
||||
const sessionSuffix = sessionMissing > 0 ? " (partial)" : "";
|
||||
const sessionLine =
|
||||
sessionCost || sessionTokens
|
||||
? `Session ${sessionCost ?? "n/a"}${sessionSuffix}${sessionTokens ? ` · ${sessionTokens} tokens` : ""}`
|
||||
: "Session n/a";
|
||||
|
||||
const todayKey = new Date().toLocaleDateString("en-CA");
|
||||
const todayEntry = summary.daily.find((entry) => entry.date === todayKey);
|
||||
const todayCost = formatUsd(todayEntry?.totalCost);
|
||||
const todayMissing = todayEntry?.missingCostEntries ?? 0;
|
||||
const todaySuffix = todayMissing > 0 ? " (partial)" : "";
|
||||
const todayLine = `Today ${todayCost ?? "n/a"}${todaySuffix}`;
|
||||
|
||||
const last30Cost = formatUsd(summary.totals.totalCost);
|
||||
const last30Missing = summary.totals.missingCostEntries;
|
||||
const last30Suffix = last30Missing > 0 ? " (partial)" : "";
|
||||
const last30Line = `Last 30d ${last30Cost ?? "n/a"}${last30Suffix}`;
|
||||
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: `💸 Usage cost\n${sessionLine}\n${todayLine}\n${last30Line}` },
|
||||
};
|
||||
}
|
||||
|
||||
if (rawArgs && !requested) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: { text: "⚙️ Usage: /usage off|tokens|full" },
|
||||
reply: { text: "⚙️ Usage: /usage off|tokens|full|cost" },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user