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
@@ -0,0 +1,14 @@
-- Fix input_tokens that were double-counted (included cache_read + cache_write tokens).
-- Subtract cache tokens to get fresh (non-cached) input only.
UPDATE sessions_v2
SET input_tokens = GREATEST(input_tokens - cache_read_tokens - cache_write_tokens, 0),
total_tokens = GREATEST(input_tokens - cache_read_tokens - cache_write_tokens, 0)
+ output_tokens + cache_read_tokens + cache_write_tokens
WHERE input_tokens > 0
AND (cache_read_tokens > 0 OR cache_write_tokens > 0);

-- Reset estimated_cost_usd to 0 so it gets recalculated by the pricing sync.
-- The pricing sync runs on startup and will recalculate all affected sessions.
UPDATE sessions_v2
SET estimated_cost_usd = 0
WHERE estimated_cost_usd > 0;
9 changes: 6 additions & 3 deletions crates/tracevault-server/src/api/session_detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ fn parse_record(record: &serde_json::Value, pricing: &ModelPricing) -> Option<Tr
}

let usage = msg.get("usage").map(|u| {
let input = u.get("input_tokens").and_then(|v| v.as_i64()).unwrap_or(0);
let total_input = u.get("input_tokens").and_then(|v| v.as_i64()).unwrap_or(0);
let output = u.get("output_tokens").and_then(|v| v.as_i64()).unwrap_or(0);
let cache_read = u
.get("cache_read_input_tokens")
Expand All @@ -144,15 +144,18 @@ fn parse_record(record: &serde_json::Value, pricing: &ModelPricing) -> Option<Tr
.get("cache_creation_input_tokens")
.and_then(|v| v.as_i64())
.unwrap_or(0);
// input_tokens from the API includes cache_read and cache_write tokens,
// so subtract them to get fresh (non-cached) input tokens only
let fresh_input = (total_input - cache_read - cache_write).max(0);
let cost = pricing::estimate_cost_with_pricing(
pricing,
input,
fresh_input,
output,
cache_read,
cache_write,
);
RecordUsage {
input_tokens: input,
input_tokens: fresh_input,
output_tokens: output,
cache_read_tokens: cache_read,
cache_write_tokens: cache_write,
Expand Down
7 changes: 5 additions & 2 deletions crates/tracevault-server/src/api/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ pub async fn handle_stream(
|| batch_cache_write > 0;
if has_tokens {
let model_name = detected_model.as_deref().unwrap_or("unknown");
// input_tokens from the API includes cache_read and cache_write,
// subtract to get fresh (non-cached) input only
let fresh_input = (batch_input - batch_cache_read - batch_cache_write).max(0);
let batch_cost = crate::pricing::estimate_cost(
model_name,
batch_input,
fresh_input,
batch_output,
batch_cache_read,
batch_cache_write,
Expand All @@ -134,7 +137,7 @@ pub async fn handle_stream(
WHERE id = $1",
)
.bind(session_db_id)
.bind(batch_input)
.bind(fresh_input)
.bind(batch_output)
.bind(batch_cache_read)
.bind(batch_cache_write)
Expand Down
20 changes: 20 additions & 0 deletions web/src/lib/components/HelpTip.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index.js';

interface Props {
text: string;
}

let { text }: Props = $props();
</script>

<Tooltip.Root>
<Tooltip.Trigger>
<span class="ml-1 cursor-help text-xs font-normal" style="color: #4f6ef7">?</span>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content class="max-w-xs text-xs">
{text}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
16 changes: 15 additions & 1 deletion web/src/lib/components/StatCard.svelte
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
<script lang="ts">
import type { Component } from 'svelte';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';

interface Props {
label: string;
value: string;
icon: Component;
color?: string;
secondary?: string;
tooltip?: string;
}

let { label, value, icon: Icon, color = '#3b82f6', secondary }: Props = $props();
let { label, value, icon: Icon, color = '#3b82f6', secondary, tooltip }: Props = $props();
</script>

<div class="bg-background rounded-lg border border-border p-4">
Expand All @@ -23,6 +25,18 @@
<div class="min-w-0">
<div class="text-muted-foreground text-[10px] font-semibold uppercase tracking-wider">
{label}
{#if tooltip}
<Tooltip.Root>
<Tooltip.Trigger>
<span class="ml-1 cursor-help" style="color: #4f6ef7">?</span>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content class="max-w-xs text-xs font-normal normal-case tracking-normal">
{tooltip}
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
{/if}
</div>
<div class="text-xl font-bold leading-tight">{value}</div>
{#if secondary}
Expand Down
4 changes: 3 additions & 1 deletion web/src/lib/components/dashboard/CacheSavingsCard.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import HelpTip from '$lib/components/HelpTip.svelte';

interface Props {
savingsUsd: number;
savingsPct: number;
Expand All @@ -10,7 +12,7 @@

<a {href} class="bg-background hover:bg-muted/50 block rounded-lg border p-4 transition-colors">
<div class="text-muted-foreground text-[11px] font-medium uppercase tracking-wide">
Cache Savings
Cache Savings<HelpTip text="Money saved by reusing cached prompt tokens instead of re-processing them at full input rate." />
</div>
<div class="mt-1 text-2xl font-bold text-green-500">
${savingsUsd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Expand Down
4 changes: 3 additions & 1 deletion web/src/lib/components/dashboard/ComplianceCard.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import HelpTip from '$lib/components/HelpTip.svelte';

interface Props {
score: number;
trend: number;
Expand All @@ -19,7 +21,7 @@

<a {href} class="bg-background hover:bg-muted/50 block rounded-lg border p-4 transition-colors">
<div class="text-muted-foreground text-[11px] font-medium uppercase tracking-wide">
Compliance
Compliance<HelpTip text="Percentage of sessions with valid cryptographic signatures, ensuring AI-generated code is properly attributed." />
</div>
<div class="mt-1 text-3xl font-bold {scoreColor}">{score.toFixed(0)}%</div>
<div class="mt-0.5 text-xs font-medium {trend >= 0 ? 'text-green-500' : 'text-amber-500'}">
Expand Down
7 changes: 6 additions & 1 deletion web/src/lib/components/dashboard/KpiCard.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import HelpTip from '$lib/components/HelpTip.svelte';

interface Props {
label: string;
value: string;
Expand All @@ -7,6 +9,7 @@
sparkline: number[];
href: string;
color?: string;
tooltip?: string;
}

let {
Expand All @@ -16,7 +19,8 @@
trendLabel,
sparkline,
href,
color = '#3b82f6'
color = '#3b82f6',
tooltip
}: Props = $props();

const trendPositive = $derived(trend >= 0);
Expand Down Expand Up @@ -48,6 +52,7 @@
>
<div class="text-muted-foreground text-[11px] font-medium uppercase tracking-wide">
{label}
{#if tooltip}<HelpTip text={tooltip} />{/if}
</div>
<div class="mt-1 text-2xl font-semibold">{value}</div>
<div class="mt-0.5 text-xs font-medium {trendPositive ? 'text-green-500' : 'text-amber-500'}">
Expand Down
8 changes: 5 additions & 3 deletions web/src/lib/components/dashboard/SessionQualityBar.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import HelpTip from '$lib/components/HelpTip.svelte';

interface Props {
avgDurationMs: number;
avgToolCalls: number;
Expand All @@ -17,15 +19,15 @@

<div class="bg-background flex items-center gap-8 rounded-lg border px-4 py-3">
<div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Avg Duration</div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Avg Duration<HelpTip text="Average wall-clock duration of sessions in this period." /></div>
<div class="mt-0.5 text-sm font-semibold">{formatDuration(avgDurationMs)}</div>
</div>
<div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Tool Calls / Session</div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Tool Calls / Session<HelpTip text="Average number of tool invocations (file edits, reads, bash commands, etc.) per session." /></div>
<div class="mt-0.5 text-sm font-semibold">{avgToolCalls.toFixed(1)}</div>
</div>
<div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Compactions / Session</div>
<div class="text-muted-foreground text-[11px] uppercase tracking-wide">Compactions / Session<HelpTip text="Average number of context window compactions per session. High values indicate long sessions that exceeded the context limit." /></div>
<div class="mt-0.5 text-sm font-semibold">{avgCompactions.toFixed(1)}</div>
</div>
</div>
9 changes: 5 additions & 4 deletions web/src/lib/components/session-detail/SessionCharts.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import Chart from '$lib/components/chart.svelte';
import HelpTip from '$lib/components/HelpTip.svelte';

interface PerCallUsage {
index: number;
Expand Down Expand Up @@ -133,7 +134,7 @@
scales: {
y: {
ticks: {
callback: (value: string | number) => `$${Number(value).toFixed(3)}`
callback: (value: string | number) => `$${Math.round(Number(value))}`
}
}
},
Expand All @@ -154,21 +155,21 @@

<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="border-border rounded-lg border p-3">
<h4 class="mb-2 text-sm font-semibold">Tokens per API Call</h4>
<h4 class="mb-2 text-sm font-semibold">Tokens per API Call<HelpTip text="Token breakdown for each API call in the session. Shows how token usage varies across the conversation." /></h4>
<div style="height: 200px">
<Chart type="bar" data={stackedBarData()} options={stackedOptions} />
</div>
</div>

<div class="border-border rounded-lg border p-3">
<h4 class="mb-2 text-sm font-semibold">Cumulative Cost</h4>
<h4 class="mb-2 text-sm font-semibold">Cumulative Cost<HelpTip text="Running total cost across all API calls. Steep sections indicate expensive calls." /></h4>
<div style="height: 200px">
<Chart type="line" data={cumulativeCostData()} options={costOptions} />
</div>
</div>

<div class="border-border rounded-lg border p-3">
<h4 class="mb-2 text-sm font-semibold">Token Distribution</h4>
<h4 class="mb-2 text-sm font-semibold">Token Distribution<HelpTip text="Overall proportion of token types. Large cache read slice indicates good cache efficiency." /></h4>
<div style="height: 200px">
<Chart type="doughnut" data={doughnutData()} options={doughnutOptions} />
</div>
Expand Down
41 changes: 29 additions & 12 deletions web/src/lib/components/session-detail/SessionDetailPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
let data: any = $state(null);
let loading = $state(true);
let error = $state('');
let showDetail = $state(false);
let showCharts = $state(true);
let showTranscript = $state(false);

async function fetchDetail() {
loading = true;
Expand Down Expand Up @@ -56,21 +57,37 @@
costBreakdown={data.cost_breakdown}
/>

{#if !showDetail}
<div class="mt-4 text-center">
<div class="mt-4 space-y-3">
<div class="border-border overflow-hidden rounded-lg border">
<button
class="bg-muted hover:bg-muted/80 border-border rounded-md border px-5 py-2 text-sm transition-colors"
style="color: #4f6ef7"
onclick={() => (showDetail = true)}
class="hover:bg-muted/40 flex w-full items-center gap-3 px-4 py-3 text-left transition-colors"
onclick={() => (showCharts = !showCharts)}
>
Show detailed charts & transcript ▼
<span class="text-muted-foreground/50 text-xs">{showCharts ? '▼' : '▶'}</span>
<span class="text-sm font-semibold">Charts</span>
</button>
{#if showCharts}
<div class="border-border border-t px-4 py-4">
<SessionCharts perCall={data.per_call} tokenDistribution={data.token_distribution} />
</div>
{/if}
</div>
{:else}
<div class="mt-4 space-y-6">
<SessionCharts perCall={data.per_call} tokenDistribution={data.token_distribution} />
<SessionTranscript records={data.transcript_records} />

<div class="border-border overflow-hidden rounded-lg border">
<button
class="hover:bg-muted/40 flex w-full items-center gap-3 px-4 py-3 text-left transition-colors"
onclick={() => (showTranscript = !showTranscript)}
>
<span class="text-muted-foreground/50 text-xs">{showTranscript ? '▼' : '▶'}</span>
<span class="text-sm font-semibold">Transcript</span>
<span class="text-muted-foreground ml-auto text-xs">{data.transcript_records.length} records</span>
</button>
{#if showTranscript}
<div class="border-border border-t px-4 py-4">
<SessionTranscript records={data.transcript_records} />
</div>
{/if}
</div>
{/if}
</div>
{/if}
</div>
Loading
Loading