fix: complete extension usage accounting

closes #6509
This commit is contained in:
Mario Zechner
2026-07-20 17:04:12 +02:00
parent 2fd3868401
commit f8b74a4507
20 changed files with 267 additions and 36 deletions
@@ -581,6 +581,37 @@ export async function generateSummary(
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
env?: Record<string, string>,
): Promise<string> {
return (
await generateSummaryWithUsage(
currentMessages,
model,
reserveTokens,
apiKey,
headers,
signal,
customInstructions,
previousSummary,
thinkingLevel,
streamFn,
env,
)
).text;
}
/** Generate or update a conversation summary and return its provider usage. */
export async function generateSummaryWithUsage(
currentMessages: AgentMessage[],
model: Model<any>,
reserveTokens: number,
apiKey: string | undefined,
headers?: Record<string, string>,
signal?: AbortSignal,
customInstructions?: string,
previousSummary?: string,
thinkingLevel?: ThinkingLevel,
streamFn?: StreamFn,
env?: Record<string, string>,
): Promise<{ text: string; usage: Usage }> {
const maxTokens = Math.min(
Math.floor(0.8 * reserveTokens),
@@ -790,7 +821,7 @@ export async function compact(
let historyText = "No prior history.";
let historyUsage: Usage | undefined;
if (messagesToSummarize.length > 0) {
const historyResult = await generateSummary(
const historyResult = await generateSummaryWithUsage(
messagesToSummarize,
model,
settings.reserveTokens,
@@ -822,7 +853,7 @@ export async function compact(
summaryUsage = historyUsage ? combineUsage(historyUsage, turnPrefixResult.usage) : turnPrefixResult.usage;
} else {
// Just generate history summary
const result = await generateSummary(
const result = await generateSummaryWithUsage(
messagesToSummarize,
model,
settings.reserveTokens,
@@ -1,4 +1,5 @@
import type { Usage } from "@earendil-works/pi-ai/compat";
import type { SessionEntry } from "./session-manager.ts";
export interface UsageTotals {
input: number;
@@ -25,3 +26,45 @@ export function addUsageToTotals(totals: UsageTotals, usage: Usage): void {
totals.cacheWrite += usage.cacheWrite;
totals.cost += usage.cost.total;
}
export interface UsageCostBreakdownEntry {
key: string;
cost: number;
tokens: number;
}
/** Group attributable assistant usage by model and all other usage into a separate bucket. */
export function getUsageCostBreakdown(entries: SessionEntry[]): UsageCostBreakdownEntry[] {
const totalsByKey = new Map<string, UsageTotals>();
for (const entry of entries) {
let key: string | undefined;
let usage: Usage | undefined;
if (entry.type === "message" && entry.message.role === "assistant") {
key = `${entry.message.provider}/${entry.message.responseModel ?? entry.message.model}`;
usage = entry.message.usage;
} else if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.usage) {
key = "Tools/summaries";
usage = entry.message.usage;
} else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
key = "Tools/summaries";
usage = entry.usage;
}
if (!key || !usage) continue;
let totals = totalsByKey.get(key);
if (!totals) {
totals = createUsageTotals();
totalsByKey.set(key, totals);
}
addUsageToTotals(totals, usage);
}
return Array.from(totalsByKey, ([key, totals]) => ({
key,
cost: totals.cost,
tokens: totals.input + totals.output + totals.cacheRead + totals.cacheWrite,
}))
.filter((entry) => entry.cost > 0 || entry.tokens > 0)
.sort((a, b) => b.cost - a.cost);
}