Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4927b3a
feat(observability): patter.* OTel span attributes (Python only) + 0.…
nicolotognoni May 8, 2026
db5d58b
fix(dashboard,pipeline): hydrate cost/latency from top-level + barge-…
nicolotognoni May 9, 2026
8a12d61
feat(barge-in): opt-in confirmation strategies (MinWords reference impl)
nicolotognoni May 9, 2026
0452c51
fix+feat: dashboard hydrate, first-audio anchor, barge-in confirmatio…
nicolotognoni May 9, 2026
c585f6d
feat(0.6.1): cost split + STT methodology fix + prewarm + 13 review f…
nicolotognoni May 10, 2026
cbe1886
fix(0.6.1): WS handoff prewarm + dashboard regressions + first-turn l…
nicolotognoni May 10, 2026
4ff09bd
fix(0.6.1): roll back STT debounce, dashboard threshold, Krisp TS sca…
nicolotognoni May 11, 2026
10a4bfa
chore: fix trailing newline in cartesia-stt.ts
nicolotognoni May 12, 2026
a510648
fix(metrics): align EOU semantics + unit (ms) across Python/TS
nicolotognoni May 12, 2026
2d628ad
feat(ts): observability OTel no-op stubs for SDK parity
nicolotognoni May 12, 2026
8b45772
fix(elevenlabs-ws-tts): close prev parked WS even when no running eve…
nicolotognoni May 12, 2026
929988d
fix(barge-in): mark first audio sent for prewarmed first message
nicolotognoni May 12, 2026
efbee51
docs(changelog): consolidate Unreleased fixes into 0.6.1 (2026-05-12)
nicolotognoni May 12, 2026
ef5e1e5
fix(0.6.1): Cerebras usage-chunk log + Krisp TS status refresh (re-ba…
nicolotognoni May 12, 2026
893a3bb
fix(0.6.1): dashboard live merge + firstMessage barge-in + drain mark…
nicolotognoni May 12, 2026
97f2a45
feat(0.6.1): OpenAI Realtime prewarm — tools + double-handshake fix +…
nicolotognoni May 12, 2026
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
610 changes: 600 additions & 10 deletions CHANGELOG.md

Large diffs are not rendered by default.

405 changes: 404 additions & 1 deletion dashboard-app/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion dashboard-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "tsc --noEmit",
"test": "vitest run",
"sync": "node ./scripts/sync.mjs"
},
"dependencies": {
Expand All @@ -21,6 +22,7 @@
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vite-plugin-singlefile": "^2.0.3"
"vite-plugin-singlefile": "^2.0.3",
"vitest": "^2.1.4"
}
}
7 changes: 6 additions & 1 deletion dashboard-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Topbar } from './components/Topbar';
import { PageHeader } from './components/PageHeader';
import { Metric, type MetricBucket } from './components/Metric';
import { CallTable, type Call } from './components/CallTable';
import { fmtCostUSD } from './components/format';
import { LiveCallPanel } from './components/LiveCallPanel';
import { MetricsPanel } from './components/MetricsPanel';
import { useDashboardData } from './hooks/useDashboardData';
Expand Down Expand Up @@ -174,6 +175,7 @@ export function App() {
spark={sparkTotalCalls.heights}
buckets={toBuckets(sparkTotalCalls)}
onSelectCall={setSelectedId}
kind="count"
/>
<Metric
label="Avg latency p95"
Expand All @@ -182,13 +184,15 @@ export function App() {
spark={sparkLatency.heights}
buckets={toBuckets(sparkLatency)}
onSelectCall={setSelectedId}
kind="latency"
/>
<Metric
label={`Spend · ${RANGE_LABEL[range]}`}
value={`$${rangeSpend.toFixed(2)}`}
value={fmtCostUSD(rangeSpend)}
spark={sparkSpend.heights}
buckets={toBuckets(sparkSpend)}
onSelectCall={setSelectedId}
kind="spend"
/>
<Metric
label="Active now"
Expand All @@ -199,6 +203,7 @@ export function App() {
spark={sparkLive.heights}
buckets={toBuckets(sparkLive)}
onSelectCall={setSelectedId}
kind="count"
/>
</div>

Expand Down
21 changes: 19 additions & 2 deletions dashboard-app/src/components/CallTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useMemo } from 'react';
import { fmtDuration, fmtPhone } from './format';
import { fmtDuration, fmtPhone, fmtCostUSD } from './format';
import { IconArrowDown, IconArrowUp, IconSearch } from './icons';

export interface CallCost {
telco?: number;
llm?: number;
stt?: number;
tts?: number;
/** @deprecated Sum of stt+tts kept for legacy aggregate-spend callers. */
sttTts?: number;
cached?: number;
total?: number;
Expand All @@ -25,12 +28,26 @@ export interface Call {
duration?: number;
latencyP95?: number;
latencyP50?: number;
/** avg(llm_ms) across this call's turns — for the waterfall llm bar. */
llmAvg?: number;
sttAvg?: number;
ttsAvg?: number;
/** Number of completed turns. p50/p95 are statistically meaningful only when this is >= 5. */
turnCount?: number;
/** p50 of agent_response_ms (wait time after user stops speaking) — user-perceived latency. */
agentResponseP50?: number;
/** p95 of agent_response_ms — user-perceived latency outlier. */
agentResponseP95?: number;
cost: CallCost;
agent?: string;
model?: string;
mode?: CallMode;
sttProvider?: string;
ttsProvider?: string;
/** Model identifier within the provider, e.g. "ink-whisper", "eleven_flash_v2_5", "gpt-oss-120b". */
sttModel?: string;
ttsModel?: string;
llmModel?: string;
transcriptKey?: string;
endedAgo?: number;
}
Expand Down Expand Up @@ -98,7 +115,7 @@ function CallRow({ call, isSelected, onSelect, isNew }: CallRowProps) {
'—'
)}
</td>
<td className="num-cell">${totalCost.toFixed(2)}</td>
<td className="num-cell">{fmtCostUSD(totalCost)}</td>
</tr>
);
}
Expand Down
52 changes: 42 additions & 10 deletions dashboard-app/src/components/CostPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,83 @@
import type { Call } from './CallTable';
import { fmtCostUSD } from './format';

export interface CostPanelProps {
call: Call | null;
}

function titleCase(s: string): string {
if (s.length === 0) return s;
// Strip provider-key transport suffixes (_ws, _rest) and role suffixes
// (_stt, _tts, _llm). Repeated `+` handles compound suffixes like
// "cartesia_tts_ws" -> "cartesia". The SDK uses provider_key like
// "elevenlabs_ws" / "cartesia_stt" to disambiguate adapter classes;
// the suffix is internal noise in user-facing UI.
const cleaned = s.replace(/(?:_(?:ws|rest|stt|tts|llm))+$/i, '');
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}

export function CostPanel({ call }: CostPanelProps) {
if (!call || !call.cost?.telco) return null;

const c = call.cost;
const telco = c.telco ?? 0;
const llm = c.llm ?? 0;
const sttTts = c.sttTts ?? 0;
const stt = c.stt ?? 0;
const tts = c.tts ?? 0;
const sttTtsLegacy = c.sttTts ?? stt + tts;
const cached = c.cached ?? 0;

const subtotal = telco + llm + sttTts;
const subtotal = telco + llm + sttTtsLegacy;
const total = subtotal - cached;
const seg = (v: number) => (subtotal > 0 ? (v / subtotal) * 100 : 0);

const sttLabel = call.sttProvider
? `${titleCase(call.sttProvider)} STT${call.sttModel ? ` · ${call.sttModel}` : ''}`
: 'STT';
const ttsLabel = call.ttsProvider
? `${titleCase(call.ttsProvider)} TTS${call.ttsModel ? ` · ${call.ttsModel}` : ''}`
: 'TTS';
const llmLabel = call.llmModel
? `${call.model ? titleCase(call.model) + ' · ' : ''}${call.llmModel}`
: call.model || 'LLM';

return (
<div className="rr-card peach">
<h3 style={{ marginBottom: 14 }}>Cost breakdown</h3>
<div className="cost-bar">
<i style={{ background: '#cc0000', width: seg(telco) + '%' }} />
<i style={{ background: '#DF9367', width: seg(llm) + '%' }} />
<i style={{ background: '#1a1a1a', width: seg(sttTts) + '%' }} />
<i style={{ background: '#1a1a1a', width: seg(stt) + '%' }} />
<i style={{ background: '#6c6c6c', width: seg(tts) + '%' }} />
</div>
<div className="stack-row">
<span className="lbl">
<span className="swatch" style={{ background: '#cc0000' }}></span>
{call.carrier === 'twilio' ? 'Twilio' : 'Telnyx'}
</span>
<span className="v">${telco.toFixed(3)}</span>
<span className="v">{fmtCostUSD(telco)}</span>
</div>
<div className="stack-row">
<span className="lbl">
<span className="swatch" style={{ background: '#DF9367' }}></span>
{call.model || 'LLM'}
{llmLabel}
</span>
<span className="v">${llm.toFixed(3)}</span>
{cached > 0 && <span className="saved">−${cached.toFixed(3)} cached</span>}
<span className="v">{fmtCostUSD(llm)}</span>
{cached > 0 && <span className="saved">−{fmtCostUSD(cached)} cached</span>}
</div>
<div className="stack-row">
<span className="lbl">
<span className="swatch" style={{ background: '#1a1a1a' }}></span>
STT / TTS
{sttLabel}
</span>
<span className="v">{fmtCostUSD(stt)}</span>
</div>
<div className="stack-row">
<span className="lbl">
<span className="swatch" style={{ background: '#6c6c6c' }}></span>
{ttsLabel}
</span>
<span className="v">${sttTts.toFixed(3)}</span>
<span className="v">{fmtCostUSD(tts)}</span>
</div>
<div className="stack-row">
<span className="lbl">
Expand All @@ -61,7 +93,7 @@ export function CostPanel({ call }: CostPanelProps) {
{call.status === 'live' ? '(running)' : ''}
</span>
</span>
<span className="v">${total.toFixed(3)}</span>
<span className="v">{fmtCostUSD(total)}</span>
</div>
</div>
);
Expand Down
54 changes: 35 additions & 19 deletions dashboard-app/src/components/LatencyPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,64 @@ export interface LatencyPanelProps {
call: Call | null;
}

// 2 turn = almeno 1 turn user genuino oltre al firstMessage. Sotto a 2 i
// percentili sono privi di senso (un singolo campione). Sopra a 2 sono
// statisticamente magri ma informativi — meglio mostrarli che lasciare il
// pannello con "—" quando la tabella sopra mostra già una p95 dal fallback
// ad avg.
const MIN_TURNS_FOR_PERCENTILES = 2;

export function LatencyPanel({ call }: LatencyPanelProps) {
if (!call || !call.latencyP95) return null;
if (!call || (!call.latencyP95 && !call.agentResponseP95)) return null;

const stt = call.sttAvg || 0;
const llm = call.latencyP50 || 0;
const tts = call.ttsAvg || 0;
const stt = call.sttAvg ?? 0;
const llm = call.llmAvg ?? 0;
const tts = call.ttsAvg ?? 0;
const total = stt + llm + tts;
const max = Math.max(total, 800);

const turns = call.turnCount ?? 0;
const showPercentiles = turns >= MIN_TURNS_FOR_PERCENTILES;
const dash = '—';

return (
<div className="rr-card">
<h3 style={{ marginBottom: 14 }}>Latency · this call</h3>
<div className="lat-grid">
<div className="latbox">
<div className="l">p50</div>
<div className="l">p50 round-trip</div>
<div className="v">
{call.latencyP50}
<span className="u">ms</span>
{showPercentiles ? call.latencyP50 ?? dash : dash}
{showPercentiles && <span className="u">ms</span>}
</div>
</div>
<div className={'latbox' + (call.latencyP95 > 600 ? ' warn' : '')}>
<div className="l">p95</div>
<div className={'latbox' + (showPercentiles && (call.latencyP95 ?? 0) > 600 ? ' warn' : '')}>
<div className="l">p95 round-trip</div>
<div className="v">
{call.latencyP95}
<span className="u">ms</span>
{showPercentiles ? call.latencyP95 ?? dash : dash}
{showPercentiles && <span className="u">ms</span>}
</div>
</div>
<div className="latbox">
<div className="l">stt avg</div>
<div className="l">p50 wait</div>
<div className="v">
{call.sttAvg}
<span className="u">ms</span>
{showPercentiles ? call.agentResponseP50 ?? dash : dash}
{showPercentiles && <span className="u">ms</span>}
</div>
</div>
<div className="latbox">
<div className="l">tts avg</div>
<div className={'latbox' + (showPercentiles && (call.agentResponseP95 ?? 0) > 600 ? ' warn' : '')}>
<div className="l">p95 wait</div>
<div className="v">
{call.ttsAvg}
<span className="u">ms</span>
{showPercentiles ? call.agentResponseP95 ?? dash : dash}
{showPercentiles && <span className="u">ms</span>}
</div>
</div>
</div>
{!showPercentiles && (
<div style={{ marginTop: -6, marginBottom: 10, fontSize: 11, opacity: 0.6 }}>
{turns} {turns === 1 ? 'turn' : 'turns'} — percentiles need ≥{MIN_TURNS_FOR_PERCENTILES}
</div>
)}

<div className="waterfall">
<div className="wf-row">
Expand Down Expand Up @@ -89,7 +105,7 @@ export function LatencyPanel({ call }: LatencyPanelProps) {
<span>
<i style={{ background: '#278EFF', opacity: 0.8 }}></i>tts
</span>
<span style={{ marginLeft: 'auto' }}>total {total} ms</span>
<span style={{ marginLeft: 'auto' }}>avg wait {Math.round(total)} ms</span>
</div>
</div>
);
Expand Down
Loading
Loading