@@ -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);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export {
|
||||
type GenerateBranchSummaryOptions,
|
||||
generateBranchSummary,
|
||||
generateSummary,
|
||||
generateSummaryWithUsage,
|
||||
getLastAssistantUsage,
|
||||
prepareBranchEntries,
|
||||
serializeConversation,
|
||||
|
||||
@@ -86,6 +86,7 @@ import type { SourceInfo } from "../../core/source-info.ts";
|
||||
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
|
||||
import type { TruncationResult } from "../../core/tools/truncate.ts";
|
||||
import { hasTrustRequiringProjectResources, ProjectTrustStore } from "../../core/trust-manager.ts";
|
||||
import { getUsageCostBreakdown } from "../../core/usage-totals.ts";
|
||||
import { getChangelogPath, getNewEntries, normalizeChangelogLinks, parseChangelog } from "../../utils/changelog.ts";
|
||||
import { copyToClipboard, readClipboardText } from "../../utils/clipboard.ts";
|
||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
|
||||
@@ -5597,22 +5598,9 @@ export class InteractiveMode {
|
||||
const cacheWaste = computeCacheWaste(entries, this.session.modelRuntime);
|
||||
|
||||
// Cost/token totals per provider/model actually used (e.g. OpenRouter `auto`
|
||||
// resolves to a concrete responseModel), sorted by cost descending.
|
||||
const perModelMap = new Map<string, { key: string; cost: number; tokens: number }>();
|
||||
for (const entry of entries) {
|
||||
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
||||
const message = entry.message;
|
||||
const usage = message.usage;
|
||||
const key = `${message.provider}/${message.responseModel ?? message.model}`;
|
||||
let bucket = perModelMap.get(key);
|
||||
if (!bucket) {
|
||||
bucket = { key, cost: 0, tokens: 0 };
|
||||
perModelMap.set(key, bucket);
|
||||
}
|
||||
bucket.cost += usage.cost.total;
|
||||
bucket.tokens += usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
||||
}
|
||||
const perModel = Array.from(perModelMap.values()).sort((a, b) => b.cost - a.cost);
|
||||
// resolves to a concrete responseModel). Usage without model attribution is
|
||||
// grouped separately so the breakdown reconciles with the session total.
|
||||
const usageBreakdown = getUsageCostBreakdown(entries);
|
||||
|
||||
let info = `${theme.bold("Session Info")}\n\n`;
|
||||
if (sessionName) {
|
||||
@@ -5646,8 +5634,8 @@ export class InteractiveMode {
|
||||
if (stats.cost > 0 || cacheWaste.missedTokens > 0) {
|
||||
info += `\n${theme.bold("Cost")}\n`;
|
||||
info += `${theme.fg("dim", "Total:")} $${stats.cost.toFixed(3)}`;
|
||||
if (perModel.length > 1) {
|
||||
for (const entry of perModel) {
|
||||
if (usageBreakdown.length > 1) {
|
||||
for (const entry of usageBreakdown) {
|
||||
info += `\n ${theme.fg("dim", `${entry.key}:`)} $${entry.cost.toFixed(3)} ${theme.fg("dim", `(${formatTokens(entry.tokens)} tokens)`)}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user