diff --git a/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql b/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql new file mode 100644 index 0000000..fe86f95 --- /dev/null +++ b/crates/tracevault-server/migrations/008_fix_double_counted_input_tokens.sql @@ -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; diff --git a/crates/tracevault-server/src/api/session_detail.rs b/crates/tracevault-server/src/api/session_detail.rs index 02b76f5..099d0bd 100644 --- a/crates/tracevault-server/src/api/session_detail.rs +++ b/crates/tracevault-server/src/api/session_detail.rs @@ -134,7 +134,7 @@ fn parse_record(record: &serde_json::Value, pricing: &ModelPricing) -> Option Option 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, @@ -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) diff --git a/web/src/lib/components/HelpTip.svelte b/web/src/lib/components/HelpTip.svelte new file mode 100644 index 0000000..915e9ba --- /dev/null +++ b/web/src/lib/components/HelpTip.svelte @@ -0,0 +1,20 @@ + + + + + ? + + + + {text} + + + diff --git a/web/src/lib/components/StatCard.svelte b/web/src/lib/components/StatCard.svelte index 2da472f..cbac94e 100644 --- a/web/src/lib/components/StatCard.svelte +++ b/web/src/lib/components/StatCard.svelte @@ -1,5 +1,6 @@
@@ -23,6 +25,18 @@
{label} + {#if tooltip} + + + ? + + + + {tooltip} + + + + {/if}
{value}
{#if secondary} diff --git a/web/src/lib/components/dashboard/CacheSavingsCard.svelte b/web/src/lib/components/dashboard/CacheSavingsCard.svelte index 251d1c6..724e329 100644 --- a/web/src/lib/components/dashboard/CacheSavingsCard.svelte +++ b/web/src/lib/components/dashboard/CacheSavingsCard.svelte @@ -1,4 +1,6 @@ @@ -72,7 +68,9 @@ {@const isActive = activeFilters.size === 0 || activeFilters.has(type)}
-
+
{#each filteredRecords as record} - +
+
{record.record_type}
+
{record.text?.trim()}
+
{/each} {#if filteredRecords.length === 0} diff --git a/web/src/routes/orgs/[slug]/analytics/+page.svelte b/web/src/routes/orgs/[slug]/analytics/+page.svelte index d2a6c96..daaa0c3 100644 --- a/web/src/routes/orgs/[slug]/analytics/+page.svelte +++ b/web/src/routes/orgs/[slug]/analytics/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { api } from '$lib/api'; import StatCard from '$lib/components/StatCard.svelte'; + import HelpTip from '$lib/components/HelpTip.svelte'; import DataTable from '$lib/components/DataTable.svelte'; import Chart from '$lib/components/chart.svelte'; import GitCommitHorizontalIcon from '@lucide/svelte/icons/git-commit-horizontal'; @@ -258,21 +259,22 @@

{error}

{:else if data}
- - - - - - - - - + + + + + + + + +

Tokens Over Time +

{#if data.tokens_over_time.length > 0}

Top Repos by Tokens +

{#if data.top_repos.length > 0}

Hourly Activity +

{#if data.hourly_activity.length > 0}

Sessions Over Time +

{#if data.sessions_over_time.length > 0}

Model Distribution +

{#if data.model_distribution.length > 0} diff --git a/web/src/routes/orgs/[slug]/analytics/attribution/+page.svelte b/web/src/routes/orgs/[slug]/analytics/attribution/+page.svelte index 279f9b8..e4b7db8 100644 --- a/web/src/routes/orgs/[slug]/analytics/attribution/+page.svelte +++ b/web/src/routes/orgs/[slug]/analytics/attribution/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { api } from '$lib/api'; import Chart from '$lib/components/chart.svelte'; + import HelpTip from '$lib/components/HelpTip.svelte'; import { Chart as ChartJS, CategoryScale, @@ -165,22 +166,22 @@
-
AI Lines
+
AI Lines
{fmtNum(data.totals.ai_lines)}
-
Human Lines
+
Human Lines
{fmtNum(data.totals.human_lines)}
-
Overall AI %
+
Overall AI %
{data.totals.ai_pct.toFixed(1)}%
-

AI vs Human Trend

+

AI vs Human Trend

{#if data.trend.length > 0}
-

By Repository

+

By Repository

{#if data.by_repo.length > 0}
-

By Author

+

By Author

{#if data.by_author.length > 0} {error}

{:else if data}
- - - + + +
-

Cost Over Time

+

Cost Over Time

{#if data.cost_over_time.length > 0}
-

Cost by Model

+

Cost by Model

{#if data.cost_by_model.length > 0}
@@ -206,7 +207,7 @@
-

Cost by Repository

+

Cost by Repository

{#if data.cost_by_repo.length > 0}
-

Cost by Author

+

Cost by Author

{#if data.cost_by_author.length > 0}
-

Model Distribution

+

Model Distribution

{#if data.distribution.length > 0}
@@ -179,7 +180,7 @@
-

Model Trends

+

Model Trends

{#if data.trends.length > 0}
-

Model Comparison

+

Model Comparison

{#if data.comparison.length > 0}
- - + + - - + +
-

Tool Frequency

+

Tool Frequency

{#if Object.keys(data.tool_frequency).length > 0} {@const entries = toolFrequencyEntries(data)} {@const total = toolFrequencyTotal(data)} diff --git a/web/src/routes/orgs/[slug]/analytics/tokens/+page.svelte b/web/src/routes/orgs/[slug]/analytics/tokens/+page.svelte index f466220..f47d676 100644 --- a/web/src/routes/orgs/[slug]/analytics/tokens/+page.svelte +++ b/web/src/routes/orgs/[slug]/analytics/tokens/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { api } from '$lib/api'; import StatCard from '$lib/components/StatCard.svelte'; + import HelpTip from '$lib/components/HelpTip.svelte'; import DataTable from '$lib/components/DataTable.svelte'; import Chart from '$lib/components/chart.svelte'; import BookOpenIcon from '@lucide/svelte/icons/book-open'; @@ -165,23 +166,26 @@ value={fmtNum(data.cache_read_tokens)} icon={BookOpenIcon} color="#3b82f6" + tooltip="Tokens served from the prompt cache at reduced cost." />
-

Tokens Over Time

+

Tokens Over Time

{#if data.time_series.length > 0}
-

By Author

+

By Author

{#if data.by_author.length > 0}
()); let expandedFiles = $state(new Set()); + let transcriptFilters = $state(new Set()); let sectionsOpen = $state({ events: true, files: false, @@ -195,6 +196,18 @@ expandedFiles = next; } + function toggleTranscriptFilter(role: string) { + const next = new Set(transcriptFilters); + if (next.has(role)) next.delete(role); + else next.add(role); + transcriptFilters = next; + } + + const ROLE_COLORS: Record = { + user: '#3ecf8e', + assistant: '#a78bfa' + }; + interface DiffLine { type: 'add' | 'remove' | 'header'; content: string; @@ -551,6 +564,7 @@

No transcript data.

{:else} {@const turns = extractTurns(data.transcript_chunks)} + {@const roleCounts = turns.reduce((acc, t) => { acc[t.role] = (acc[t.role] || 0) + 1; return acc; }, {} as Record)} {#if turns.length === 0}
{#each data.transcript_chunks as chunk (chunk.chunk_index)} @@ -558,8 +572,21 @@ {/each}
{:else} +
+ {#each Object.entries(roleCounts) as [role, count]} + {@const color = ROLE_COLORS[role] || '#6b7594'} + {@const isActive = transcriptFilters.size === 0 || transcriptFilters.has(role)} + + {/each} +
- {#each turns as turn, i} + {#each turns.filter(t => transcriptFilters.size === 0 || transcriptFilters.has(t.role)) as turn}