fix: align compaction cuts and native usage accounting
This commit is contained in:
@@ -333,6 +333,7 @@ export async function handleChatInput(
|
||||
outputTokens: usage.output,
|
||||
cacheReadTokens: usage.cacheRead,
|
||||
cacheWriteTokens: usage.cacheWrite,
|
||||
cacheWrite1hTokens: usage.cacheWrite1h ?? 0,
|
||||
// Optional in pi's Usage — persisted as 0, never dropped.
|
||||
reasoningTokens: usage.reasoning ?? 0,
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ async function compact(deps: CompactionDeps, chatId: string): Promise<void> {
|
||||
if (tokensBefore <= deps.thresholdTokens) return; // settled beneath while we waited
|
||||
|
||||
const cut = compactionCutIndex(messages);
|
||||
if (cut >= messages.length) return; // nothing to drop (defensive)
|
||||
if (cut <= 0 || cut >= messages.length) return; // no history can be dropped
|
||||
|
||||
// The previous compaction's kept boundary (chained compactions
|
||||
// summarize the old summary + what rode after it).
|
||||
@@ -240,6 +240,7 @@ async function compact(deps: CompactionDeps, chatId: string): Promise<void> {
|
||||
outputTokens: usage.output,
|
||||
cacheReadTokens: usage.cacheRead,
|
||||
cacheWriteTokens: usage.cacheWrite,
|
||||
cacheWrite1hTokens: usage.cacheWrite1h ?? 0,
|
||||
reasoningTokens: usage.reasoning ?? 0,
|
||||
},
|
||||
],
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ import {
|
||||
} from "../core/graph-deletion.ts";
|
||||
import { extractionStatuses } from "../agent/subagents/extraction.ts";
|
||||
import { stores } from "./stores.ts";
|
||||
import { callCost } from "./pricing.ts";
|
||||
|
||||
/**
|
||||
* Server-side graph entry for the home view (GET /api/graph). A PURE READ:
|
||||
@@ -41,7 +42,7 @@ export function graphCosts(): GraphCostSummary {
|
||||
.readSources()
|
||||
.filter((s) => s.extractions?.length)
|
||||
.map((s) => ({ filePath: s.filePath, extractions: s.extractions! }));
|
||||
return graphCostSummary({ chats, sources });
|
||||
return graphCostSummary({ chats, sources }, callCost);
|
||||
}
|
||||
|
||||
/** The graph's version stamp (session-deletion wave 6): the max mtime
|
||||
|
||||
@@ -43,3 +43,4 @@ export * from "./compaction.ts";
|
||||
export { runPiTurn, piTurnResponse, type PiTurnHandle, type PiTurnSpec } from "./pi/ui-stream.ts";
|
||||
export * from "./observability.ts";
|
||||
export * from "./model-catalog.ts";
|
||||
export { sessionCost } from "./pricing.ts";
|
||||
|
||||
@@ -24,6 +24,7 @@ import { chatStore } from "../store/chats.ts";
|
||||
import { graphRevision } from "./graph.ts";
|
||||
import { catalogContextLimits } from "./model-catalog.ts";
|
||||
import { listJobs } from "./jobs.ts";
|
||||
import { jobCost } from "./pricing.ts";
|
||||
import type { JobRecord } from "../core/contracts.ts";
|
||||
|
||||
export { abortExtraction, extractionStatuses, type ExtractionPhase, type RunningExtraction } from "../agent/subagents/extraction.ts";
|
||||
@@ -36,6 +37,7 @@ export { chatStore } from "../store/chats.ts";
|
||||
* model is missing from the catalog (the blob then renders no context
|
||||
* line — never a fake window). */
|
||||
export interface ActivityJob extends JobRecord {
|
||||
costUsd: number | null;
|
||||
title: string;
|
||||
contextLimit: number | null;
|
||||
}
|
||||
@@ -56,9 +58,8 @@ export interface ActivityExtraction {
|
||||
|
||||
/** The dock's whole world in one payload: every running process, every view.
|
||||
* Jobs are RUNNING-ONLY — finished jobs belong to their chat's transcript;
|
||||
* the blob disappears when the process ends. Cost math is NOT done here:
|
||||
* raw usage rides on the records and the client folds it with the pure
|
||||
* helpers in core/prices.ts (transport stays thin). */
|
||||
* the blob disappears when the process ends. Costs are joined here through
|
||||
* the server-side Pi pricing adapter; clients never reconstruct rates. */
|
||||
/** The dock's whole world + the graph's version stamp (wave 6 session
|
||||
* deletion): the payload carries graphRevision (max mtime across the
|
||||
* graph data files — see server/graph.ts) so any view can detect "the
|
||||
@@ -78,6 +79,7 @@ export function activityFeed(): {
|
||||
if (job.status === "running") {
|
||||
jobs.push({
|
||||
...job,
|
||||
costUsd: jobCost(job),
|
||||
title: chat.title ?? chat.id,
|
||||
contextLimit: job.usage ? (limits.get(job.usage.model) ?? null) : null,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/** Read-time API-dollar estimates. Pi owns both rates and arithmetic.
|
||||
* Catalog resolution is key-independent; no provider dispatch/auth is needed.
|
||||
* Never mutate saved native messages: calculateCost mutates its usage.cost.
|
||||
*/
|
||||
import { calculateCost, type Usage } from "@earendil-works/pi-ai";
|
||||
import { piModels } from "../agent/models.ts";
|
||||
import type { CallUsage, ChatRecord, JobRecord, TurnUsage } from "../core/contracts.ts";
|
||||
import { isChatGPTPlan, sessionCost as foldSessionCost, turnCost as foldTurnCost, jobCost as foldJobCost } from "../core/prices.ts";
|
||||
|
||||
function catalogModel(id: string) {
|
||||
const colon = id.indexOf(":");
|
||||
if (colon < 0 || isChatGPTPlan(id)) return undefined;
|
||||
return piModels().getModel(id.slice(0, colon), id.slice(colon + 1));
|
||||
}
|
||||
|
||||
export function callCost(call: Partial<CallUsage>, modelId: string): number | null {
|
||||
const model = catalogModel(modelId);
|
||||
if (!model || call.inputTokens === undefined || call.outputTokens === undefined) return null;
|
||||
const input = call.inputTokens;
|
||||
const output = call.outputTokens;
|
||||
const cacheRead = call.cacheReadTokens ?? 0;
|
||||
const cacheWrite = call.cacheWriteTokens ?? 0;
|
||||
const cacheWrite1h = call.cacheWrite1hTokens ?? 0;
|
||||
if ([input, output, cacheRead, cacheWrite, cacheWrite1h].some((v) => !Number.isFinite(v) || v < 0) || cacheWrite1h > cacheWrite) return null;
|
||||
// Old records discarded this split. Only price them if it cannot affect
|
||||
// the result; do not invent short-retention precision from a missing field.
|
||||
if (cacheWrite > 0 && call.cacheWrite1hTokens === undefined &&
|
||||
[model.cost, ...(model.cost.tiers ?? [])].some((rate) => rate.cacheWrite !== 2 * rate.input)) return null;
|
||||
const usage: Usage = {
|
||||
input, output, cacheRead, cacheWrite, cacheWrite1h,
|
||||
totalTokens: input + output + cacheRead + cacheWrite,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
const total = calculateCost(model, usage).total;
|
||||
return Number.isFinite(total) && total >= 0 ? total : null;
|
||||
}
|
||||
|
||||
export function turnCost(turn: TurnUsage): number | null {
|
||||
return foldTurnCost(turn, callCost);
|
||||
}
|
||||
export function sessionCost(record: Pick<ChatRecord, "turns">) {
|
||||
return foldSessionCost(record, callCost);
|
||||
}
|
||||
export function jobCost(job: Pick<JobRecord, "usage">): number | null {
|
||||
const usage = job.usage;
|
||||
if (!usage || usage.steps < 1) return null;
|
||||
const hasCalls = usage.calls?.length === usage.steps;
|
||||
if (!hasCalls && usage.steps > 1 && catalogModel(usage.model)?.cost.tiers?.length) return null;
|
||||
return foldJobCost(job, callCost);
|
||||
}
|
||||
Reference in New Issue
Block a user