Files
Logan CusanoandClaude Opus 5.5 0543526eb0 gemini: minimal thinking on correlation, token accounting per call
A day of replay runs (server-26#170) spent ~$5 of Gemini on ~7 two-hour
windows (~$0.70 per 290 calls) — several dollars a day per live deployment
for correlation alone — and nothing could say where it went (#45). Gemini
3.x thinks by default and bills it as output; the deprecated
google-generativeai SDK these calls used cannot set a thinking level.

- app/internal/gemini.py: every Gemini call (correlation + transcript
  correction) goes through google-genai with JSON mode, an explicit
  thinking level, and logs in/out/thinking tokens. A model that rejects
  the level is retried without it once and remembered, so the tier is
  never lost to a config param. API failures still raise for ai_health.
- correlator: thinking_level "minimal" (a link/new/orphan choice).
  transcript correction: "low" until a replay shows minimal is safe.
- replay: runs record real Gemini token usage (metrics.gemini_usage),
  shown in the Replay tab.
- requirements: google-genai.

c2-core: 474 pass. Frontend typecheck not run (no Node on this box).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:10:40 -04:00

458 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// ---------------------------------------------------------------------------
// Replay — re-run the intelligence pipeline over a past time range into a
// sandbox, then compare runs. Backend: drb-c2-core/app/internal/replay.py.
// Nothing here touches a live call or incident; every run spends real AI
// credits, so the form estimates first and only then offers Start.
// ---------------------------------------------------------------------------
import { useCallback, useEffect, useState } from "react";
import { c2api } from "@/lib/c2api";
import { useSystems } from "@/lib/useSystems";
import type {
ReplayEstimate, ReplayIncident, ReplayIncidents, ReplayMode, ReplayRun, ReplayCallRow,
} from "@/lib/types";
const MODES: { key: ReplayMode; label: string; help: string }[] = [
{ key: "transcripts", label: "Saved transcripts", help: "Reuse each call's transcript; re-run extraction + correlation." },
{ key: "audio", label: "Re-transcribe audio", help: "Whisper the saved audio again, then extract + correlate. For ranges where AI was off." },
{ key: "reuse", label: "Correlation only", help: "Reuse an earlier run's extraction; re-run correlation only. Isolates a correlator change." },
];
// Clears that came from the radio traffic itself, vs the 90-minute idle timer.
const SIGNAL_RESOLVES = ["units_cleared", "llm_closure", "reassignment", "children_resolved"];
const input =
"bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm font-mono focus:outline-none focus:border-indigo-500";
const btn =
"bg-gray-800 hover:bg-gray-700 disabled:opacity-50 border border-gray-700 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors";
function toLocalInput(d: Date): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function fmtTime(iso: string | null | undefined): string {
if (!iso) return "—";
return new Date(iso).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}
function sum(rec: Record<string, number> | undefined, keys: string[]): number {
return keys.reduce((n, k) => n + (rec?.[k] ?? 0), 0);
}
// ---------------------------------------------------------------------------
function NewRunForm({ runs, busy, onStarted }: { runs: ReplayRun[]; busy: boolean; onStarted: () => void }) {
const { systems } = useSystems();
const [from, setFrom] = useState(() => toLocalInput(new Date(Date.now() - 6 * 3600_000)));
const [to, setTo] = useState(() => toLocalInput(new Date()));
const [mode, setMode] = useState<ReplayMode>("transcripts");
const [systemIds, setSystemIds] = useState<string[]>([]);
const [sourceRun, setSourceRun] = useState("");
const [label, setLabel] = useState("");
const [est, setEst] = useState<ReplayEstimate | null>(null);
const [working, setWorking] = useState(false);
const [error, setError] = useState<string | null>(null);
// Any change to what would be replayed invalidates the estimate.
useEffect(() => { setEst(null); }, [from, to, mode, systemIds]);
const sources = runs.filter((r) => r.status === "done" && r.mode !== "reuse");
const range = () => ({ date_from: new Date(from).toISOString(), date_to: new Date(to).toISOString() });
async function estimate() {
setWorking(true); setError(null);
try {
setEst(await c2api.estimateReplay({ ...range(), mode, system_ids: systemIds }));
} catch (e) { setError(String(e)); } finally { setWorking(false); }
}
async function start() {
setWorking(true); setError(null);
try {
await c2api.startReplay({
...range(), mode, system_ids: systemIds, label,
source_run_id: mode === "reuse" ? sourceRun : null,
});
setEst(null); setLabel("");
onStarted();
} catch (e) { setError(String(e)); } finally { setWorking(false); }
}
function toggleSystem(id: string) {
setSystemIds((s) => (s.includes(id) ? s.filter((x) => x !== id) : [...s, id]));
}
const canStart = est && !est.truncated && est.calls > 0 && !busy && (mode !== "reuse" || sourceRun);
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-4">
<div className="flex flex-wrap items-end gap-4">
<div>
<label className="text-xs text-gray-400 block mb-1">From</label>
<input type="datetime-local" value={from} max={to} onChange={(e) => setFrom(e.target.value)} className={input} />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">To</label>
<input type="datetime-local" value={to} min={from} onChange={(e) => setTo(e.target.value)} className={input} />
</div>
<div>
<label className="text-xs text-gray-400 block mb-1">Label</label>
<input value={label} onChange={(e) => setLabel(e.target.value)} placeholder="what changed?" className={`${input} w-56`} />
</div>
</div>
<div className="space-y-1">
{MODES.map((m) => (
<label key={m.key} className="flex items-start gap-2 text-sm font-mono cursor-pointer">
<input type="radio" checked={mode === m.key} onChange={() => setMode(m.key)} className="mt-1" />
<span className="text-white">{m.label}</span>
<span className="text-gray-500 text-xs mt-0.5">{m.help}</span>
</label>
))}
</div>
{mode === "reuse" && (
<div>
<label className="text-xs text-gray-400 block mb-1">Reuse extraction from</label>
<select value={sourceRun} onChange={(e) => setSourceRun(e.target.value)} className={input}>
<option value="">— pick a finished run —</option>
{sources.map((r) => (
<option key={r.run_id} value={r.run_id}>
{r.label || r.run_id} · {fmtTime(r.date_from)} → {fmtTime(r.date_to)}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">Set the range to match that run; calls outside it are skipped.</p>
</div>
)}
{systems.length > 1 && (
<div className="flex flex-wrap gap-3">
<span className="text-xs text-gray-400">Systems (none = all):</span>
{systems.map((s) => (
<label key={s.system_id} className="flex items-center gap-1 text-xs font-mono text-gray-300 cursor-pointer">
<input type="checkbox" checked={systemIds.includes(s.system_id)} onChange={() => toggleSystem(s.system_id)} />
{s.name}
</label>
))}
</div>
)}
<div className="flex flex-wrap items-center gap-3">
<button onClick={estimate} disabled={working} className={btn}>{working && !est ? "Counting…" : "Estimate"}</button>
<button
onClick={start}
disabled={!canStart || working}
className="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white text-sm font-mono px-4 py-1.5 rounded-lg transition-colors"
>
Start run
</button>
{busy && <span className="text-xs text-amber-400 font-mono">a run is in progress</span>}
</div>
{est && (
<p className="text-sm font-mono text-gray-300">
{est.truncated ? (
<span className="text-red-400">More than {est.max_calls} calls — narrow the range.</span>
) : (
<>
<span className="text-white">{est.calls}</span> calls ·{" "}
{est.calls_with_transcript} with transcripts · {est.audio_minutes} audio min ·{" "}
<span className="text-amber-400">~${est.est_cost_usd.toFixed(2)}</span> est.
{mode === "transcripts" && est.calls_with_transcript < est.calls / 2 && (
<span className="text-amber-400"> — most calls have no transcript; consider Re-transcribe audio.</span>
)}
</>
)}
</p>
)}
{error && <p className="text-red-400 text-sm font-mono">{error}</p>}
</div>
);
}
// ---------------------------------------------------------------------------
function RunsTable({
runs, activeId, selected, onSelect, onChanged,
}: {
runs: ReplayRun[]; activeId: string | null; selected: string | null;
onSelect: (id: string) => void; onChanged: () => void;
}) {
async function cancel(id: string) {
await c2api.cancelReplay(id).catch(() => undefined);
onChanged();
}
async function remove(id: string) {
if (!window.confirm("Delete this run and its sandbox incidents?")) return;
await c2api.deleteReplay(id).catch(() => undefined);
onChanged();
}
if (!runs.length) return <p className="text-sm text-gray-500 font-mono">No runs yet.</p>;
const th = "text-left text-xs text-gray-500 font-normal px-2 py-1.5 whitespace-nowrap";
const td = "px-2 py-1.5 whitespace-nowrap";
return (
<div className="overflow-x-auto">
<table className="w-full text-sm font-mono">
<thead>
<tr className="border-b border-gray-800">
<th className={th}>Run</th>
<th className={th}>Range</th>
<th className={th}>Status</th>
<th className={th} title="Incidents created">Inc</th>
<th className={th} title="Share of incidents that are a single call">1-call</th>
<th className={th} title="Calls that never linked to an incident">Orphans</th>
<th className={th} title="Closed by radio traffic (units cleared, closure, reassignment) vs the idle timer">Real clears / timeouts</th>
<th className={th} title="Correlation decisions the LLM took part in">LLM</th>
<th className={th}>Cost</th>
<th className={th}></th>
</tr>
</thead>
<tbody>
{runs.map((r) => {
const m = r.metrics;
const running = r.run_id === activeId;
return (
<tr
key={r.run_id}
onClick={() => onSelect(r.run_id)}
className={`border-b border-gray-800/60 cursor-pointer ${selected === r.run_id ? "bg-gray-800/60" : "hover:bg-gray-900"}`}
>
<td className={td}>
<div className="text-white">{r.label || r.run_id}</div>
<div className="text-xs text-gray-500">{r.mode} · {r.git_sha?.slice(0, 7)} · {fmtTime(r.created_at)}</div>
</td>
<td className={`${td} text-xs text-gray-400`}>{fmtTime(r.date_from)} → {fmtTime(r.date_to)}</td>
<td className={td}>
{running ? (
<span className="text-amber-400">{r.progress.done}/{r.progress.total}</span>
) : (
<span className={r.status === "done" ? "text-green-400" : "text-red-400"}>{r.status}</span>
)}
{r.progress.errors > 0 && <span className="text-red-400 text-xs"> · {r.progress.errors} err</span>}
</td>
<td className={td}>{m?.incidents ?? "—"}</td>
<td className={td}>{m?.single_call_pct != null ? `${m.single_call_pct}%` : "—"}</td>
<td className={td}>{m ? `${m.calls_orphaned}/${m.calls}` : "—"}</td>
<td className={td}>
{m ? (
<>
<span className="text-green-400">{sum(m.resolved_via, SIGNAL_RESOLVES)}</span>
{" / "}
<span className="text-gray-400">{m.resolved_via.idle_timeout ?? 0}</span>
</>
) : "—"}
</td>
<td className={td}>{m?.llm_decisions ?? "—"}</td>
<td className={td}>{m ? `$${m.est_cost_usd.toFixed(2)}` : `~$${r.estimate.est_cost_usd.toFixed(2)}`}</td>
<td className={td} onClick={(e) => e.stopPropagation()}>
{running ? (
<button onClick={() => cancel(r.run_id)} className="text-xs text-amber-400 hover:text-amber-300">cancel</button>
) : (
<button onClick={() => remove(r.run_id)} className="text-xs text-gray-500 hover:text-red-400">delete</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
// ---------------------------------------------------------------------------
function CallLine({ call }: { call: ReplayCallRow }) {
const [audio, setAudio] = useState<string | null>(null);
async function play() {
try {
const c = await c2api.getCall(call.call_id);
setAudio(c.audio_url);
} catch { /* audio is a convenience */ }
}
return (
<div className="py-1.5 border-t border-gray-800/60 text-xs font-mono">
<div className="flex flex-wrap gap-2 text-gray-500">
<span>{fmtTime(call.started_at)}</span>
<span>{call.talkgroup_name}</span>
{call.corr_path.map((p, i) => <span key={i} className="text-indigo-400">{p}</span>)}
{call.units?.length ? <span>units {call.units.join(", ")}</span> : null}
{call.cleared_units?.length ? <span className="text-green-400">cleared {call.cleared_units.join(", ")}</span> : null}
{call.skip_reason && <span className="text-gray-600">{call.skip_reason}</span>}
{audio ? (
<audio src={audio} controls autoPlay className="h-6" />
) : (
<button onClick={play} className="text-gray-400 hover:text-white">▶ audio</button>
)}
</div>
<div className="text-gray-200 mt-0.5">{call.transcript || <span className="text-gray-600">(no transcript)</span>}</div>
</div>
);
}
function IncidentCard({ inc }: { inc: ReplayIncident }) {
const [open, setOpen] = useState(false);
const signal = inc.resolved_via && SIGNAL_RESOLVES.includes(inc.resolved_via);
return (
<div className="bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
<button onClick={() => setOpen(!open)} className="w-full text-left flex flex-wrap items-center gap-x-3 gap-y-1 text-sm font-mono">
<span className="text-gray-500">{open ? "▾" : "▸"}</span>
<span className="text-white">{inc.title || inc.incident_id}</span>
<span className="text-gray-500 text-xs">{inc.calls.length} call{inc.calls.length !== 1 ? "s" : ""}</span>
<span className="text-gray-500 text-xs">{fmtTime(inc.started_at)} → {fmtTime(inc.resolved_at)}</span>
<span className={`text-xs ${signal ? "text-green-400" : "text-gray-500"}`}>{inc.resolved_via ?? inc.status}</span>
{inc.location_coords && <span className="text-xs text-indigo-400">📍 {inc.location}</span>}
</button>
{open && <div className="mt-2">{inc.calls.map((c) => <CallLine key={c.call_id} call={c} />)}</div>}
</div>
);
}
function RunDetail({ run }: { run: ReplayRun }) {
const [data, setData] = useState<ReplayIncidents | null>(null);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<"all" | "multi" | "single">("all");
const [showOrphans, setShowOrphans] = useState(false);
useEffect(() => {
setData(null); setError(null);
if (run.status === "running") return;
c2api.getReplayIncidents(run.run_id).then((d) => {
setData(d);
// Exposed for in-page analysis (console / automation) of a run's
// sandbox — the same data this tab renders, nothing more.
(window as unknown as { __drbReplay?: unknown }).__drbReplay = { run, ...d };
}).catch((e) => setError(String(e)));
}, [run.run_id, run.status]);
const m = run.metrics;
const shown = (data?.incidents ?? []).filter((i) =>
filter === "all" ? true : filter === "multi" ? i.calls.length > 1 : i.calls.length === 1,
);
return (
<div className="space-y-3">
{m && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs font-mono">
{[
["Linked calls", `${m.calls_linked}/${m.calls}`],
["Median calls / incident", m.median_calls_per_incident ?? "—"],
["With units cleared", m.incidents_with_units_cleared],
["With map pin", m.incidents_with_coords],
].map(([k, v]) => (
<div key={k as string} className="bg-gray-900 border border-gray-800 rounded-lg p-2">
<div className="text-gray-500">{k}</div>
<div className="text-white text-base">{v}</div>
</div>
))}
</div>
)}
{m && (
<p className="text-xs font-mono text-gray-500">
resolved: {Object.entries(m.resolved_via).map(([k, v]) => `${k} ${v}`).join(" · ")}
<br />
paths: {Object.entries(m.corr_path).map(([k, v]) => `${k} ${v}`).join(" · ")}
</p>
)}
{m?.gemini_usage && Object.keys(m.gemini_usage).length > 0 && (
<p className="text-xs font-mono text-gray-500">
Gemini tokens:{" "}
{Object.entries(m.gemini_usage)
.map(([k, u]) => `${k} ${u.calls} calls, in ${u.in.toLocaleString()} / out ${u.out.toLocaleString()} / thinking ${u.thinking.toLocaleString()}`)
.join(" · ")}
</p>
)}
{m?.ai_failures && Object.keys(m.ai_failures).length > 0 && (
<p className="text-xs font-mono text-amber-400">
AI failures: {Object.entries(m.ai_failures).map(([k, v]) => `${k} ×${v}`).join(" · ")}
</p>
)}
{run.errors?.length > 0 && (
<details className="text-xs font-mono text-red-400">
<summary>{run.errors.length} error(s)</summary>
{run.errors.map((e, i) => <div key={i}>{e}</div>)}
</details>
)}
{run.status === "running" && <p className="text-sm text-gray-500 font-mono">Incidents appear when the run finishes.</p>}
{error && <p className="text-red-400 text-sm font-mono">{error}</p>}
{data && (
<>
<div className="flex gap-1 text-xs font-mono">
{(["all", "multi", "single"] as const).map((f) => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 rounded-md ${filter === f ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"}`}
>
{f === "all" ? `All (${data.incidents.length})` : f === "multi" ? "Multi-call" : "Single-call"}
</button>
))}
<button
onClick={() => setShowOrphans(!showOrphans)}
className={`px-3 py-1 rounded-md ${showOrphans ? "bg-gray-800 text-white" : "text-gray-500 hover:text-gray-300"}`}
>
Orphans ({data.orphans.length})
</button>
</div>
{showOrphans ? (
<div className="bg-gray-900 border border-gray-800 rounded-lg px-3 py-2">
{data.orphans.map((c) => <CallLine key={c.call_id} call={c} />)}
</div>
) : (
<div className="space-y-1.5">{shown.map((i) => <IncidentCard key={i.incident_id} inc={i} />)}</div>
)}
</>
)}
</div>
);
}
// ---------------------------------------------------------------------------
export function ReplayTab() {
const [runs, setRuns] = useState<ReplayRun[]>([]);
const [activeId, setActiveId] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
try {
const res = await c2api.listReplays();
setRuns(res.runs);
setActiveId(res.active_run_id);
setError(null);
} catch (e) { setError(String(e)); }
}, []);
useEffect(() => { load(); }, [load]);
// Poll only while something is running.
useEffect(() => {
if (!activeId) return;
const t = setInterval(load, 4000);
return () => clearInterval(t);
}, [activeId, load]);
const run = runs.find((r) => r.run_id === selected) ?? null;
return (
<div className="space-y-5">
<p className="text-xs text-gray-500 font-mono">
Re-run the pipeline over a past time range, in the calls&apos; original order, into a sandbox — live incidents are never
touched. Replay the same range after each change and compare the rows below. A run starts with no incidents
open, so its first ~90 minutes split more than live did — compare runs over the same range, not a run against live.
</p>
<NewRunForm runs={runs} busy={!!activeId} onStarted={load} />
{error && <p className="text-red-400 text-sm font-mono">{error}</p>}
<RunsTable runs={runs} activeId={activeId} selected={selected} onSelect={setSelected} onChanged={load} />
{run && <RunDetail run={run} />}
</div>
);
}