Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -206,15 +206,6 @@ test('copies bounded evidence for the exact failed Turn', async () => {
message: 'No endpoints accepted the request',
},
],
totals: {
durationMs: 3_501,
modelAttempts: 1,
retries: 0,
compactions: 0,
inputTokens: 0,
outputTokens: 0,
unpricedAttempts: 1,
},
failure: {
code: 'model_call_failed',
message: 'No endpoints accepted the request',
Expand Down
131 changes: 117 additions & 14 deletions apps/desktop/src/main/__tests__/session-inspector-panel-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
SESSION_TRACE_SCHEMA_VERSION,
emptyTraceTotals,
type SessionTrace,
type TraceModelAttempt,
type TraceModelCallStep,
type TraceStep,
} from '@maka/core/session-trace';
import { deriveInspectorPanelModel } from '../../renderer/session-inspector-panel-model.js';
import {
Expand Down Expand Up @@ -118,6 +120,59 @@ test('does not estimate a cache-hit ratio from partial usage', () => {
assert.equal(overview.cacheHitRate, undefined);
});

test('derives per-turn cost only from priced model-call step totals', () => {
const cases: readonly {
name: string;
steps: TraceStep[];
expected: number | undefined;
}[] = [
{ name: 'empty', steps: [], expected: undefined },
{
name: 'tool-only',
steps: [
{
kind: 'tool',
id: 'tool-1',
turnId: 'turn-1',
runId: 'run-1',
startedAt: 1,
endedAt: 2,
durationMs: 1,
toolName: 'Read',
status: 'completed',
},
],
expected: undefined,
},
{ name: 'unpriced', steps: [modelCallStep('unpriced')], expected: undefined },
{ name: 'priced', steps: [modelCallStep('priced', 0.01)], expected: 0.01 },
{
name: 'mixed',
steps: [modelCallStep('priced', 0.01), modelCallStep('unpriced')],
expected: 0.01,
},
{
name: 'multiple calls',
steps: [modelCallStep('first', 0.01), modelCallStep('second', 0.02)],
expected: 0.03,
},
{ name: 'zero-priced', steps: [modelCallStep('free', 0)], expected: 0 },
{
name: 'retried logical call',
// Deliberately disagree with the nested attempts: the display boundary
// must trust the logical call's already-aggregated price.
steps: [modelCallStep('retry', 0.04, [modelAttempt(0, 0.01), modelAttempt(1, 0.02)])],
expected: 0.04,
},
];

for (const { name, steps, expected } of cases) {
const turn = deriveInspectorPanelModel(traceWithSteps(steps)).turns[0];
assert.equal(turn?.costUsd, expected, name);
assert.equal(turn?.durationMs, 9, `${name} duration`);
}
});

test('shows one compact diagnostic line for a failed history-compaction call', () => {
const trace: SessionTrace = {
schemaVersion: SESSION_TRACE_SCHEMA_VERSION,
Expand Down Expand Up @@ -163,20 +218,8 @@ test('shows one compact diagnostic line for a failed history-compaction call', (
],
},
],
totals: {
...emptyTraceTotals(),
durationMs: 9,
modelAttempts: 1,
unpricedAttempts: 1,
},
},
],
totals: {
...emptyTraceTotals(),
durationMs: 9,
modelAttempts: 1,
unpricedAttempts: 1,
},
coverage: {
modelCalls: 'no_known_gap',
turnsMissingModelCalls: [],
Expand All @@ -201,7 +244,6 @@ test('reports runs omitted only by the bounded online view separately', () => {
schemaVersion: SESSION_TRACE_SCHEMA_VERSION,
sessionId: 'session-1',
turns: [],
totals: emptyTraceTotals(),
coverage: {
modelCalls: 'partial',
turnsMissingModelCalls: [],
Expand All @@ -219,3 +261,64 @@ test('reports runs omitted only by the bounded online view separately', () => {
oversizedRuns: 1,
});
});

function traceWithSteps(steps: TraceStep[]): SessionTrace {
return {
schemaVersion: SESSION_TRACE_SCHEMA_VERSION,
sessionId: 'session-1',
turns: [
{
turnId: 'turn-1',
runId: 'run-1',
startedAt: 1,
endedAt: 10,
durationMs: 9,
steps,
},
],
coverage: {
modelCalls: 'no_known_gap',
turnsMissingModelCalls: [],
turnsWithFewerModelCallsThanSteps: [],
unreadableRecords: 0,
oversizedRuns: 0,
},
};
}

function modelCallStep(
id: string,
costUsd?: number,
attempts: TraceModelAttempt[] = [modelAttempt(0, costUsd)],
): TraceModelCallStep {
return {
kind: 'model_call',
id,
turnId: 'turn-1',
runId: 'run-1',
startedAt: 1,
endedAt: 10,
durationMs: 9,
callKind: 'main',
providerId: 'provider-1',
modelId: 'model-1',
step: 0,
attempts,
status: 'completed',
...(costUsd !== undefined ? { costUsd } : {}),
};
}

function modelAttempt(attempt: number, costUsd?: number): TraceModelAttempt {
return {
attemptId: `attempt-${attempt}`,
attempt,
status: 'completed',
startedAt: 1,
completedAt: 10,
latencyMs: 9,
...(costUsd !== undefined ? { costUsd } : {}),
costBasis: costUsd === undefined ? 'unpriced' : 'priced',
usageBasis: 'reported',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,9 @@ import {
SESSION_TRACE_SCHEMA_VERSION,
type SessionTrace,
type TraceModelAttempt,
type TraceTotals,
} from '@maka/core/session-trace';
import { deriveInspectorPanelModel } from '../../renderer/session-inspector-panel-model.js';

const TOTALS: TraceTotals = {
durationMs: 10,
modelAttempts: 1,
retries: 0,
compactions: 0,
inputTokens: 1,
outputTokens: 1,
unpricedAttempts: 1,
};

describe('Session Inspector Pricing key', () => {
test('uses the canonical provider rather than the connection slug for an unpriced call', () => {
const model = deriveInspectorPanelModel(traceWithAttempt(attempt('unpriced')));
Expand Down Expand Up @@ -76,16 +65,8 @@ function traceWithAttempt(modelAttempt: TraceModelAttempt): SessionTrace {
...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01 } : {}),
},
],
totals: {
...TOTALS,
...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01, unpricedAttempts: 0 } : {}),
},
},
],
totals: {
...TOTALS,
...(modelAttempt.costBasis === 'priced' ? { costUsd: 0.01, unpricedAttempts: 0 } : {}),
},
coverage: {
modelCalls: 'no_known_gap',
turnsMissingModelCalls: [],
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/main/__tests__/use-session-trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { afterEach, describe, it } from 'node:test';
import { act, createElement } from 'react';
import {
SESSION_TRACE_SCHEMA_VERSION,
emptyTraceTotals,
type SessionTrace,
} from '@maka/core/session-trace';
import type { SessionEvent } from '@maka/core/events';
Expand All @@ -30,7 +29,6 @@ function trace(sessionId: string): SessionTrace {
schemaVersion: SESSION_TRACE_SCHEMA_VERSION,
sessionId,
turns: [],
totals: emptyTraceTotals(),
coverage: {
modelCalls: 'none',
turnsMissingModelCalls: [],
Expand Down Expand Up @@ -197,7 +195,6 @@ function tracePage(
endedAt: startedAt,
durationMs: 0,
steps: [],
totals: emptyTraceTotals(),
},
],
},
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,6 @@ async function loadSessionTracePage(
schemaVersion: page.schemaVersion,
sessionId,
turns: [...page.turns],
totals: page.totals,
coverage: page.coverage,
};
if (!isSessionTrace(trace)) throw new Error('Invalid Session trace projection');
Expand Down
36 changes: 24 additions & 12 deletions apps/desktop/src/renderer/session-inspector-panel-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
type SessionTrace,
type TraceModelCallStep,
type TraceStep,
type TraceTotals,
} from '@maka/core/session-trace';
import { pricingModelKey } from '@maka/core/usage-stats/pricing';
/**
Expand Down Expand Up @@ -45,7 +44,7 @@ export interface InspectorTurnRow {
turnId: string;
startedAt: number;
durationMs: number;
totals: TraceTotals;
costUsd?: number;
failed: boolean;
failureCode?: string;
steps: InspectorStepRow[];
Expand Down Expand Up @@ -73,16 +72,19 @@ export interface InspectorPanelModel {
export function deriveInspectorPanelModel(trace: SessionTrace | undefined): InspectorPanelModel {
if (!trace) return { turns: [], empty: true };

const turns = trace.turns.map<InspectorTurnRow>((turn) => ({
runId: turn.runId,
turnId: turn.turnId,
startedAt: turn.startedAt,
durationMs: turn.durationMs,
totals: turn.totals,
failed: turn.failure !== undefined,
...(turn.failure?.code !== undefined ? { failureCode: turn.failure.code } : {}),
steps: turn.steps.map((step) => toStepRow(step, turn.failure?.attributedToStepId)),
}));
const turns = trace.turns.map<InspectorTurnRow>((turn) => {
const costUsd = deriveTurnCostUsd(turn.steps);
return {
runId: turn.runId,
turnId: turn.turnId,
startedAt: turn.startedAt,
durationMs: turn.durationMs,
...(costUsd !== undefined ? { costUsd } : {}),
failed: turn.failure !== undefined,
...(turn.failure?.code !== undefined ? { failureCode: turn.failure.code } : {}),
steps: turn.steps.map((step) => toStepRow(step, turn.failure?.attributedToStepId)),
};
});

const coverage = coverageNotice(trace);
return {
Expand All @@ -95,6 +97,16 @@ export function deriveInspectorPanelModel(trace: SessionTrace | undefined): Insp
};
}

function deriveTurnCostUsd(steps: readonly TraceStep[]): number | undefined {
let total: number | undefined;
for (const step of steps) {
if (step.kind === 'model_call' && step.costUsd !== undefined) {
total = (total ?? 0) + step.costUsd;
}
}
return total;
}

function toStepRow(step: TraceStep, attributedToStepId: string | undefined): InspectorStepRow {
const failed =
step.id === attributedToStepId ||
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/session-inspector-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ function TurnRow(props: {
<span className="maka-inspector-turn-meta">
{formatDuration(turn.durationMs)} ·{' '}
<span className="maka-inspector-turn-cost">
{formatCost(turn.totals.costUsd, copy.costUnavailable)}
{formatCost(turn.costUsd, copy.costUnavailable)}
</span>
</span>
</div>
Expand Down
Loading