inspect-calls-and-metrics — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited inspect-calls-and-metrics (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Patter persists every call to an in-memory MetricsStore (500-call ring buffer by default), optionally backed by disk. The dashboard surfaces live calls, transcripts, latency breakdowns, per-leg cost, and recordings. You can also pull CallMetrics programmatically and export to CSV/JSON for offline analysis.
Patter ships a dashboard route you can mount on the same server as your agent. Visit http://localhost:8000/dashboard to see live calls.
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
async def main():
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="...",
first_message="Hi!",
)
# dashboard=True mounts /dashboard (UI) + /api/calls (REST) + /sse (live stream)
await phone.serve(agent, tunnel=True, dashboard=True)
asyncio.run(main())import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "...",
firstMessage: "Hi!",
});
await phone.serve({ agent, tunnel: true, dashboard: true });Open http://localhost:8000/dashboard. Live calls appear at the top, with real-time transcript, current cost, and latency p50/p90/p95/p99.
CallMetrics is the canonical model — every finished call produces one. The hook is `on_call_end` passed as a kwarg to phone.serve(...). It receives a dict (the CallMetrics serialized form), and is async.
import asyncio
from getpatter import Patter, Twilio, OpenAIRealtime2
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
async def on_end(metrics: dict) -> None:
print(f"Call {metrics['call_id']} | {metrics['duration_seconds']:.1f}s")
cost = metrics.get("cost", {})
print(f" Cost ${cost.get('total_usd', 0):.4f}: "
f"STT ${cost.get('stt_usd', 0):.4f} · "
f"LLM ${cost.get('llm_usd', 0):.4f} · "
f"TTS ${cost.get('tts_usd', 0):.4f} · "
f"Realtime ${cost.get('realtime_usd', 0):.4f} · "
f"Telephony ${cost.get('telephony_usd', 0):.4f}")
print(f" Latency p99: {metrics.get('latency_p99', 0):.0f} ms")
agent = phone.agent(engine=OpenAIRealtime2(), system_prompt="...", first_message="Hi!")
asyncio.run(phone.serve(agent, tunnel=True, on_call_end=on_end))import { Patter, Twilio, OpenAIRealtime2 } from "getpatter";
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "...",
firstMessage: "Hi!",
});
await phone.serve({
agent,
tunnel: true,
onCallEnd: async (metrics) => {
console.log(`Call ${metrics.call_id} | ${metrics.duration_seconds.toFixed(1)}s`);
console.log(` Cost $${metrics.cost?.total_usd?.toFixed(4) ?? "0"}`);
console.log(` Latency p99: ${metrics.latency_p99?.toFixed(0) ?? "0"} ms`);
},
});You can also pull live data programmatically from the MetricsStore attached to the server — phone.metrics_store (Python) / phone.metricsStore (TypeScript). It returns None/null until serve() has bound; once the server is live, you can iterate calls.
calls_to_csv and calls_to_json operate on the MetricsStore exposed on the running server.
from getpatter import calls_to_csv, calls_to_json
store = phone.metrics_store # available after serve() has bound
if store is not None:
with open("calls.csv", "w") as f:
f.write(calls_to_csv(store))
import json
with open("calls.json", "w") as f:
json.dump(calls_to_json(store), f, indent=2)import { callsToCsv, callsToJson } from "getpatter";
import { writeFileSync } from "fs";
const store = phone.metricsStore; // available after serve() has bound
if (store) {
writeFileSync("calls.csv", callsToCsv(store));
writeFileSync("calls.json", JSON.stringify(callsToJson(store), null, 2));
}| Route | Purpose |
|---|---|
GET /api/calls | List recent calls (newest first). |
GET /api/calls/{call_id} | Detailed metrics + transcript for one call. |
GET /sse | Server-Sent Events stream of live call updates. |
Example with curl:
curl http://localhost:8000/api/calls | jq '.[] | {id: .call_id, cost: .cost.total_usd, duration: .duration_seconds}'By default the ring buffer is in-memory. Pass persist=True (or a path) to mirror to disk:
phone = Patter(
carrier=Twilio(),
phone_number="+15550001234",
persist=True, # writes to ./.patter/calls.db
# persist="/var/patter/calls.db", # or a custom path
)const phone = new Patter({
carrier: new Twilio(),
phoneNumber: "+15550001234",
persist: true, // writes to ./.patter/calls.db
});When exposing the dashboard beyond 127.0.0.1, gate it with a bearer token. Patter ships token auth out of the box via the dashboard_token kwarg — all /dashboard, /api/calls, and /sse routes then require Authorization: Bearer <token>.
await phone.serve(agent, tunnel=True, dashboard_token="hunter2-rotate-me")await phone.serve({ agent, tunnel: true, dashboardToken: "hunter2-rotate-me" });For programmatic FastAPI/Express embedding (not just phone.serve), the lower-level make_auth_dependency(...) (Python) / makeAuthMiddleware(...) (TypeScript) factories are also exported — useful when you mount the dashboard onto your own app and need a custom auth scheme.
Without auth, do not bind to `0.0.0.0` — call transcripts are PII.
CallMetricsKey fields (see models.py / metrics.ts for the full list):
| Field | Meaning |
|---|---|
call_id | UUID, persistent across the call. |
duration_seconds | Total call wall time. |
turns | List of TurnMetrics (user/agent each). |
cost | CostBreakdown(stt_usd, llm_usd, tts_usd, realtime_usd, telephony_usd, total_usd). |
latency_avg / p50 / p90 / p95 / p99 | Turn-time percentiles in ms. |
provider_mode | "openai_realtime", "elevenlabs_convai", "pipeline". |
stt_provider / llm_provider / tts_provider / telephony_provider | Provider identifiers. |
stt_model / llm_model / tts_model | Model strings. |
transcript | List of (role, text) tuples — set when input_audio_transcription_model is configured. |
Recording URLs live in the carrier's payload, surfaced in the call.recording.saved log line — they are not exposed as a typed CallMetrics field in 0.6.3. Enable recording with phone.serve(..., recording=True).
memory (still on disk if persist=True). For long-running production, use the on-disk SQLite to query history.
in production.
push to a queue for slow exporters.
percentile estimator needs ~100 samples; for shorter calls use latency_avg.
pricing.py at SDK release time. If a provider changes pricing mid-quarter, your cost.*_usd will drift. Use merge_pricing({...}) to override.
| Symptom | Fix |
|---|---|
| Dashboard 404s | You passed dashboard=False to phone.serve(...). The default is True — drop the kwarg. |
| Dashboard shows no calls | Server restarted with persistence off. persist=True is the default in 0.6.3; verify you didn't override it. |
| Transcript is empty | Realtime mode without input_audio_transcription_model set, or guardrail blocked the response. |
| Recording URL is missing from logs | recording=True wasn't passed to phone.serve(...), or the carrier doesn't have recording enabled. |
cost.total_usd is 0 | The provider isn't in Patter's pricing table. Override per-provider rates via merge_pricing({...}) on the Patter constructor. |
build-voice-agent — agents must exist before they produce metrics.add-tools-and-handoffs — tools' per-call latency shows in the dashboard.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.