fix: preserve unknown graph costs and cache detail
This commit is contained in:
@@ -303,7 +303,7 @@ function graphUsageLabel(costs: Pick<GraphCostSummary, "stages">): string {
|
|||||||
const plan = stages.some((stage) => (stage.subscriptionRuns ?? 0) > 0);
|
const plan = stages.some((stage) => (stage.subscriptionRuns ?? 0) > 0);
|
||||||
const priced = stages.some((stage) => stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) > 0);
|
const priced = stages.some((stage) => stage.runs - stage.unpricedRuns - (stage.subscriptionRuns ?? 0) > 0);
|
||||||
const dollars = displayCostTotals(costs).totalDollars;
|
const dollars = displayCostTotals(costs).totalDollars;
|
||||||
return plan ? (priced ? `$${dollars.toFixed(2)} API + ChatGPT plan` : "ChatGPT plan") : `$${dollars.toFixed(2)}`;
|
return plan ? (priced ? `$${dollars.toFixed(2)} API + ChatGPT plan` : "ChatGPT plan") : priced ? `$${dollars.toFixed(2)}` : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ function stageDollars(model: string, steps: readonly CallUsage[], price: CallPri
|
|||||||
/** Merge one step-set's tokens into an accumulating CallUsage. */
|
/** Merge one step-set's tokens into an accumulating CallUsage. */
|
||||||
function mergeTokens(into: CallUsage, steps: readonly CallUsage[]): void {
|
function mergeTokens(into: CallUsage, steps: readonly CallUsage[]): void {
|
||||||
const summed = sumSteps(steps);
|
const summed = sumSteps(steps);
|
||||||
|
// Fold this before updating total writes, so an older write-bearing
|
||||||
|
// aggregate with a missing split remains unknown even after fresh data.
|
||||||
|
into.cacheWrite1hTokens = sumSteps([into, summed]).cacheWrite1hTokens;
|
||||||
into.inputTokens = (into.inputTokens ?? 0) + (summed.inputTokens ?? 0);
|
into.inputTokens = (into.inputTokens ?? 0) + (summed.inputTokens ?? 0);
|
||||||
into.outputTokens = (into.outputTokens ?? 0) + (summed.outputTokens ?? 0);
|
into.outputTokens = (into.outputTokens ?? 0) + (summed.outputTokens ?? 0);
|
||||||
into.cacheReadTokens = (into.cacheReadTokens ?? 0) + (summed.cacheReadTokens ?? 0);
|
into.cacheReadTokens = (into.cacheReadTokens ?? 0) + (summed.cacheReadTokens ?? 0);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React from "react";
|
||||||
|
(globalThis as Record<string, unknown>).React = React;
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { GraphCostControl } from "../components/graph-view.tsx";
|
||||||
|
import { graphCostSummary } from "../src/core/cost-summary.ts";
|
||||||
|
import { callCost } from "../src/server/pricing.ts";
|
||||||
|
import type { CallUsage, ExtractionRunUsage } from "../src/core/contracts.ts";
|
||||||
|
|
||||||
|
const usage = (over: Partial<CallUsage> = {}): CallUsage => ({ inputTokens: 1000, outputTokens: 100, cacheReadTokens: 0, cacheWriteTokens: 20_000, reasoningTokens: 0, ...over });
|
||||||
|
const run = (steps: CallUsage[], model = "anthropic:claude-haiku-4-5", at = 1): ExtractionRunUsage => ({ startedAt: at, at: "fixture", durationMs: 1, stages: { extractor: { model, steps }, auditor: { model, steps } } });
|
||||||
|
const summary = (runs: ExtractionRunUsage[]) => graphCostSummary({ chats: [{ title: "fixture", extractions: runs }], sources: [] }, callCost);
|
||||||
|
const headline = (runs: ExtractionRunUsage[]) => renderToStaticMarkup(<GraphCostControl costs={summary(runs)} />);
|
||||||
|
|
||||||
|
test("wholly unpriced historical graph costs render unknown, not known-zero dollars", () => {
|
||||||
|
const historical = run([usage()]); // missing long-cache split
|
||||||
|
const costs = summary([historical]);
|
||||||
|
assert.equal(costs.totalDollars, 0);
|
||||||
|
assert.equal(costs.unpricedRuns, 1);
|
||||||
|
assert.equal(costs.stages.extraction.unpricedRuns, 1);
|
||||||
|
const html = headline([historical]);
|
||||||
|
assert.match(html, />—<\/button>/);
|
||||||
|
assert.doesNotMatch(html, /\$0\.00/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("known zero, mixed priced/unknown, and subscription headline semantics stay distinct", () => {
|
||||||
|
const zero = run([usage({ inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0 })]);
|
||||||
|
assert.match(headline([zero]), />\$0\.00<\/button>/, "known zero is not unknown");
|
||||||
|
const priced = run([usage({ cacheWrite1hTokens: 0 })]);
|
||||||
|
const historical = run([usage()], undefined, 2);
|
||||||
|
const mixed = summary([priced, historical]);
|
||||||
|
assert.equal(mixed.unpricedRuns, 1);
|
||||||
|
assert.ok(mixed.totalDollars > 0);
|
||||||
|
assert.match(headline([priced, historical]), new RegExp(`>\\$${mixed.totalDollars.toFixed(2).replace(".", "\\.")}<\\/button>`));
|
||||||
|
const plan = run([usage()], "chatgpt:fixture");
|
||||||
|
assert.match(headline([plan]), />ChatGPT plan<\/button>/);
|
||||||
|
assert.match(headline([plan, zero]), />\$0\.00 API \+ ChatGPT plan<\/button>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("graph stage and recent-run aggregates preserve known one-hour cache writes", () => {
|
||||||
|
const costs = summary([run([usage({ cacheWrite1hTokens: 20_000 })]), run([usage({ cacheWrite1hTokens: 5000 })], undefined, 2)]);
|
||||||
|
assert.equal(costs.stages.extraction.tokens.cacheWrite1hTokens, 25_000);
|
||||||
|
assert.equal(costs.stages.audit.tokens.cacheWrite1hTokens, 25_000);
|
||||||
|
assert.deepEqual(costs.runs.map((r) => r.tokens.cacheWrite1hTokens), [10_000, 40_000]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown historical long-cache splits stay unknown regardless of merge order", () => {
|
||||||
|
const known = run([usage({ cacheWrite1hTokens: 20_000 })]);
|
||||||
|
const historical = run([usage()], undefined, 2);
|
||||||
|
for (const runs of [[known, historical], [historical, known]]) {
|
||||||
|
const costs = summary(runs);
|
||||||
|
assert.equal(costs.stages.extraction.tokens.cacheWrite1hTokens, undefined);
|
||||||
|
assert.equal(costs.stages.audit.tokens.cacheWrite1hTokens, undefined);
|
||||||
|
}
|
||||||
|
const mixedWithinRun = summary([run([usage({ cacheWrite1hTokens: 20_000 }), usage()])]);
|
||||||
|
assert.equal(mixedWithinRun.stages.extraction.tokens.cacheWrite1hTokens, undefined);
|
||||||
|
const absentNoWrites = run([usage({ cacheWriteTokens: 0 })], undefined, 2);
|
||||||
|
assert.equal(summary([absentNoWrites, known]).stages.extraction.tokens.cacheWrite1hTokens, 20_000, "absence without writes must not poison known write detail");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user