of over-budget customers ({{ (intakeSegment.over_budget_count||0).toLocaleString() }})
this segment{{ (intakeSegment.total_customers||0).toLocaleString() }} customers total
${{ t.budget }} budget · {{ (t.segment||0).toLocaleString() }} of {{ (t.over_budget||0).toLocaleString() }} over
Rate over time{{ intakeSegTrend.label }}bars = customers stuck per day · amber line = % stuck · last {{ SEG_CHART_DAYS }} days
Customers whose entire budget was consumed by the intake steps — the business-context form, screenshot prompt, length-continuation and build-retry loops — before they ever submitted a genuine request.
Retries over time{{ editsSegTrend.label }}bars = retries per day · amber line = % of requests · last {{ SEG_CHART_DAYS }} days (UTC)
Turns where the builder re-prompted the model because a file edit it had already proposed failed to apply — full price paid to redo work a previous turn already paid for.
{{ (editsSegment.analyzed_as_human||0).toLocaleString() }} of them were scored as customer sentiment, distorting {{ editsSegment.distorted_reports.toLocaleString() }} report{{ editsSegment.distorted_reports === 1 ? '' : 's' }}.
{{ bestDaySpendMultiple.toFixed(1) }}× a typical day (${{ formatMoney(spotlightTypicalSpend) }})
in line with the ${{ formatMoney(spotlightTypicalSpend) }} typical day
· {{ bestDay.success_rate }}% success that day
One OpenVPN client per person, each in its own network namespace, each reaching one proxy host. A profile applies only after you approve it here for its workspace's proxy; a changed profile or a changed Base URL waits again. A route marked scheduled sync is the one its workspace's nightly run rides.
Quick refresh brings in recent requests and updates spend, tokens and customer inventory. Conversation details and classification update in the background. History sync and full repair also prepare older data.
Cancel did not take effect — {{ syncCancelNotice }}
{{ syncStatus.db_count.toLocaleString() }} requests
· {{ syncStatus.db_oldest }} → {{ syncStatus.db_latest }}
· Last sync run {{ relativeTime(syncStatus.last_sync_at) }}⚠ failedNo sync recorded yet
Recent requests: {{ syncQuickStatus }}
{{ syncQuickFreshness }}
Conversation details: {{ syncPreparationStatus }}
Next sync: {{ syncStatus.next_sync_preview }}
· first sync may take several minutes
{{ syncCollectionProgress }}
Unfinished history:
{{ syncStatus.carryover.days }} day{{ syncStatus.carryover.days === 1 ? '' : 's' }}
({{ syncStatus.carryover.oldest === syncStatus.carryover.newest ? syncStatus.carryover.newest : syncStatus.carryover.oldest + ' to ' + syncStatus.carryover.newest }})
· this sync continues collecting it
· the next sync starts right away and fetches them first
· the next scheduled sync fetches them first
{{ syncCarryoverProgress }}
Data may be stale — last sync was more than {{ syncStatus.stale_threshold_hours }}h ago
Couldn't reach the server for sync status. Showing the last status received.
Deployment in progress. New syncs and customer-data preparation are paused until it finishes.
Quick refresh pauses background preparation while it updates recent calls.{{ syncStatus.lock_name || 'Another job' }} holds the data lock since {{ syncHoldSince }} — no sync is running. New syncs wait until it finishes; nothing here needs to be cancelled or retried.
Starting sync. The latest saved run stays visible until the new run reports its tasks.
The workspace is syncing. Showing the last saved run until current task details arrive.
Connection interrupted. Showing the last saved task states; retrying automatically.
{{ syncStatus.running && !syncOtherHold ? 'Sync is running. Waiting for its saved task details.' : 'A sync will report its tasks here when it starts.' }}
{{ syncStatus.carryover.days }} day{{ syncStatus.carryover.days === 1 ? '' : 's' }} still to fetch.
{{ syncCollectionActive ? 'This sync continues collecting it before preparing the customer data.' : syncStatus.carryover.continues ? 'The next continuation pass starts automatically.' : 'The next scheduled sync fetches them first.' }}
{{ syncCarryoverProgress }}
Sync on a schedule, so the dashboard stays current without anyone starting it.
Up to {{ syncScheduleMaxTimes }} a day, at least {{ syncScheduleMinGap }} minutes apart.
{{ syncScheduleError }}
Only workspace admins can change the automatic sync schedule.
A shared LiteLLM workspace key is required before enabling the schedule.
{{ reconHeadline }} · {{ reconInProgress }} still settling
Daily total short ({{ reconciliation.summary.daily_short }}) — LiteLedger holds every call for {{ reconciliation.summary.daily_short === 1 ? 'that day' : 'those days' }}. LiteLLM's own daily total is ${{ reconDailyShortSpend.toFixed(2) }} lower in all, because its daily table left out calls that its per-call log recorded. No sync can change LiteLLM's daily total, so these days are settled. Spend charts count those calls: each day uses the larger of the two totals.
Upstream gap ({{ reconciliation.summary.upstream_gap }}) — LiteLLM's daily total for {{ reconciliation.summary.upstream_gap === 1 ? 'that day' : 'those days' }} counts requests whose individual log entries no longer exist there, proven by the difference staying identical to the cent across repeated re-fetches on separate syncs. Spend charts use the larger of the two totals for each day. If the upstream numbers ever move, the day is retried automatically.
Historical ({{ reconciliation.summary.historical }}) — before {{ upstreamOldestDay || 'the oldest day' }} LiteLLM still holds per-call logs for, so individual requests can no longer be re-fetched. Spend charts use the larger of the two totals for each day. The days LiteLedger itself keeps are a separate matter: nothing synced is dropped here unless a retention period is set.
{{ reconciliation.summary.drift }} day{{ reconciliation.summary.drift === 1 ? '' : 's' }} still reconciling — cost that had not finished settling upstream when the rows were captured. Later syncs re-fetch {{ reconciliation.summary.drift === 1 ? 'it' : 'them' }} automatically until the numbers agree; spend charts use the larger of the two totals meanwhile.
Sync History
Loading…
No sync events recorded yet.
Couldn't load this run's output: {{ syncHistoryLogErrors[entry.id] }}
This run is still going — its output is saved as it progresses.
This run stopped before it recorded any output.
This run was refused before it started — {{ entry.error }}. Nothing was fetched and nothing was written.
No output was kept for this run.
The product your customers use, as analyses and Ask name it.
✓ Saved{{ productError }}
Current setting: {{ (settings.product || {}).name }}, {{ (settings.product || {}).description }}.
Only your workspace admin can change this.
How it works
Each analysis stage and each Ask answer tells the model which product the customers use.
With no name set, that is the default: {{ (settings.product_default || {}).name }}, {{ (settings.product_default || {}).description }}.
A name without a description reads as an AI-powered product. A change applies to analyses and answers
from then on; existing reports keep the wording they were written with.
Gives each customer a segment from when they were first seen and their budget, shown as a column and filter on the Customers page.
✓ Saved{{ customerSegmentsError }}
Current setting: {{ settings.customer_segments !== false ? 'On' : 'Off' }}.
Only your workspace admin can change this.
How it works
GTM: first seen on or after {{ SEGMENT_RULE_VALUES.day }} (UTC), the public
launch, with a budget under {{ SEGMENT_RULE_VALUES.cap }}. Soft launch: first seen before then, with per-call
history and a budget under {{ SEGMENT_RULE_VALUES.cap }}. Legacy: first seen before then, with per-call history and
a budget of {{ SEGMENT_RULE_VALUES.cap }} or more. Prehistoric: first seen before then and known only from daily
spend totals, whatever the budget. Employee: tagged in
Settings → Customer Mappings, which takes precedence over the others. A customer that matches none has no
segment.
Off, no customer has a segment, and Ask counts every customer as unclassified.
Sorts customers into power, medium, light and minimal users by their total requests, for the tier cards, column and filter on the Customers page.
✓ Saved. The Customers page regroups once its list is rebuilt.{{ customerTiersError }}
Current setting: {{ customerTiersSummary }}
Only your workspace admin can change this.
How it works
A customer's tier comes from their total requests on record, human and machine: power from the first number up,
then medium and light, and minimal below the light number. A customer with no activity for more than the first
number of days drops one tier, and for more than the second number two; minimal is the lowest.
The defaults are {{ customerTiersDefaultText }}.
A change regroups every customer once the Customers list is rebuilt, which takes a few minutes on a large
workspace; until then the page shows the previous grouping with its own thresholds. Reports don't change, and
Ask's tiers, which count human messages, keep a fixed scale of their own.
Workspace Timezone
{{ settings.workspace_timezone || 'UTC' }}
The IANA timezone used to localize dashboard and report timestamps for everyone in this workspace.
✓ Saved{{ workspaceTimezoneError }}
Current setting: {{ settings.workspace_timezone || 'UTC' }}.
Only your workspace admin can change this.
Analysis Window
{{ windowLabel(settings.analysis_window_days) }}
Recent reviews the selected number of days ending at the customer's latest message.
All available history reviews their history stored in LiteLedger.
This setting selects the default for new analyses and the preferred dashboard report.
If that report is unavailable, the other period's report is used.
You can choose a different time range when starting an analysis.
Saving…✓ SavedApplies to new analyses.
Switch to All available history — new analyses will cover each
customer's stored history. Longer histories may take more time and cost more.
Existing history reports become the preferred dashboard reports.
Switch to {{ windowLabel(analysisWindowForm) }} — new
analyses will cover the last {{ windowLabel(analysisWindowForm) }} of each
customer's activity. Existing recent reports become the preferred dashboard reports.
Current setting: {{ windowLabel(settings.analysis_window_days) }}.
Only your workspace admin can change this.
Re-analyzes customers automatically when they send new messages.
How it works
It also re-analyzes a customer when a classification change lowers their human message
count by a fifth or more, and analyzes customers who have messages but no analysis yet.
Customers that Ask queued to refresh go first, then the rest, highest spend first. It runs
as many at a time as the Analyses at once per person setting allows, and starts nothing
new while any other analysis is queued or running.
Up toanalyses a daySaving…✓ Saved
{{ upkeepStatusLine }}
{{ upkeepError }}
Background analysis is {{ settings.analysis_upkeep.enabled ? 'on' : 'off' }} for this workspace.
Only your workspace admin can change this.
Ask Answer Confidence
chip color only
Sets when the data-coverage chip on an Ask answer reads amber vs. red. These
thresholds change the chip color only — never the numbers shown, which
are always the true coverage of your analyzed customers.
A cohort answer covering less than this share of spend reads red (low coverage). Default 60%.
When most of the analyzed set is outdated, an otherwise-amber answer drops to red. Default 50%.
Saving…✓ Saved
Amber at ≥ {{ Math.round((settings.ask_confidence_spend_floor ?? 0.60)*100) }}% of spend covered;
red when more than {{ Math.round((settings.ask_confidence_stale_ceil ?? 0.50)*100) }}% of analyses are stale.
Only your workspace admin can change these.
Controls how verbose Stage 3 reports are. Denser levels generate fewer output tokens natively —
saving is realized on every analysis run. All structured fields (scores, enums, frequencies,
feature names, categories, priorities) stay byte-identical across levels; only prose density changes.
Saving…✓ Saved — new analyses will use {{ CAVEMAN_LABELS[settings.default_caveman_level || 'standard'].label }}Changes apply to every user at @{{ (settings.admin_email || '').split('@')[1] || 'your domain' }}. Takes effect on the next analysis run.
{{ CAVEMAN_LABELS[level].label }}
●
{{ CAVEMAN_LABELS[level].hint }}
Only your workspace admin can change this. Current level: {{ CAVEMAN_LABELS[settings.default_caveman_level || 'standard'].label }}.
Controls how long detailed request records are kept locally after syncing.
Spend aggregates (daily totals) are always kept regardless of this setting.
Unlocking in {{ retentionUnlockCountdown }}…Saving…✓ SavedUnlocked
Switch to Unlimited — no records will be deleted.
Switch to {{ retentionLabel(retentionDaysForm) }} —
longer than the current setting, so no records will be deleted.
Switch to {{ retentionLabel(retentionDaysForm) }} —
request records older than {{ retentionLabel(retentionDaysForm) }} will be permanently deleted
on the next sync. Spend aggregates are unaffected.
Counting affected records…
{{ retentionPreviewCount.toLocaleString() }} {{ retentionPreviewCount === 1 ? 'record' : 'records' }} will be deleted.
Current setting: {{ settings.requests_retention_days ? retentionLabel(settings.requests_retention_days) : 'Unlimited' }}.
Only your workspace admin can change this.
How the pipeline separates a real human turn from the automated machine calls it triggers, before sentiment analysis runs. Every message starts as Machine; a rule is what makes one Human.
Machine rules win. When a message matches both a Machine and a Human rule, it is Machine — whatever order the rules are in. That is why a Human rule is allowed to be broad: a narrower Machine rule pulls the false positives back. To force one specific message the other way, correct it on the customer page; a manual correction outranks every rule.
No messages would change classification — another rule already covers everything this one matches.
{{ rulePreview['editor'].result.to_human.count.toLocaleString() }} would flip to Human ·
{{ rulePreview['editor'].result.to_machine.count.toLocaleString() }} would flip to Machine
Removing this rule wouldn't change any classifications — another rule already covers these messages.
{{ rulePreview['remove:' + rule.id].result.to_human.count.toLocaleString() }} message{{ rulePreview['remove:' + rule.id].result.to_human.count === 1 ? '' : 's' }} would flip to Human ·
{{ rulePreview['remove:' + rule.id].result.to_machine.count.toLocaleString() }} would flip to Machine
No messages would change classification — another rule already covers everything this one matches.
{{ rulePreview['add:' + i].result.to_human.count.toLocaleString() }} message{{ rulePreview['add:' + i].result.to_human.count === 1 ? '' : 's' }} would flip to Human ·
{{ rulePreview['add:' + i].result.to_machine.count.toLocaleString() }} would flip to Machine
Re-scanned {{ rescanResult.rescanned }}/{{ rescanResult.total_customers }} customer{{ rescanResult.total_customers === 1 ? '' : 's' }}.
{{ rescanResult.changed_count }} {{ rescanResult.changed_count === 1 ? 'has' : 'have' }} a different live count under current rules.
No changes — current rules already applied everywhere.
{{ rescanResult.stale_count }} analyzed report{{ rescanResult.stale_count === 1 ? '' : 's' }} flagged stale — re-run those to apply the new rules.
{{ c.customer }}:
{{ c.before }} → {{ c.after }}
…and {{ rescanResult.changed_count - 8 }} more
Rules changed since the last scan.
Re-applies all rules to every customer's messages and reports the impact.
Model Lab
Tune local open-source models + inference settings against frontier quality (Haiku / Sonnet) on your real pipeline prompts. Opus 4.8 judges every output — absolute rubric + blind head-to-head vs the frontier reference.
1 · Models
Open-source models under test
{{ m.model_id }} · {{ m.via }}
scanning…{{ bench.discoverErr }}
Frontier reference (the bar to clear)
{{ m.model_id }}
Judge: {{ bench.graderModel }} (absolute rubric + blind head-to-head). The reference is generated once per customer and reused.
Associate requests made through shared virtual keys with the customer they serve. Rules read an identifier from request tags and match it to a customer account. The original virtual key remains attached to every request.
Shared service calls remain machine activity in the customer journey. Missing or conflicting identifiers stay with their source and are listed for review.
Served through: {{ attribution.testResult.source_key_alias || attribution.testResult.key_alias }}
Preview draft revision {{ attribution.revision }}
Dates limit this preview. Activation applies the saved rules to all retained requests and future syncs. Totals cover every matching request in the selected range; examples are a sample.
Revision {{ revision.revision }} · {{ formatTime(revision.activated_at) }} · Active
Mappings
Named lookup sets keyed by account number — the customer webID map, the employee tag list,
and any custom set you create (emails, plan tiers, region codes, …).
Read-only — workspace admins manage entries.
Pairs of lines: - accountId: followed by - {{ msCsvValueHeader }}:.
One entry per line, or separated by commas, spaces or tabs. Numeric entries are treated as
account numbers; anything else is looked up as a webID. Importing only adds accounts.
…and {{ msImportPreview.warnings.length - 8 }} more
Couldn't resolve {{ msImportPreview.unresolved.length }} entr{{ msImportPreview.unresolved.length === 1 ? 'y' : 'ies' }} to an account
(no matching webID — these will be skipped):
The URL of your LiteLLM proxy instance — shared across everyone at your domain.
✓ Saved
✓ Saved
You're an admin — changes apply to every user at @{{ (settings.admin_email || '').split('@')[1] || 'your domain' }}.
Managed by your admin. A personal key overrides it.
Your admin hasn't set a shared key yet. A personal key works in its place.
My VPN Route
Personal
How this server reaches the proxy for you.
{{ vpnStateLabel(settings.route.state) }}
An OpenVPN profile you upload here authenticates the server as you, so only you can see, change or remove it,
and only requests you make ride it — syncs you start, Test Connection, and Ask when it uses the LiteLLM route.
No teammate's requests use your route, whatever their role.
This profile authenticates the server as you; anyone with root on it can use it while it is valid. The platform admin approves it for one proxy host before anything connects.
Dropped from the upload (the namespace owns these): {{ settings.route.dropped_directives.join(', ') }}
{{ settings.route.rewrites.join(' · ') }}
{{ settings.route.warnings.join(' ') }}
The workspace's Base URL changed since this profile was approved (approved for
{{ settings.route.pinned_to }}, now {{ settings.route.current_target }}).
The route stays down until the platform admin approves it for the new host.
Waiting for the platform admin to approve this profile for {{ settings.route.current_target || 'the proxy' }} before the route is applied.
The temporary conduit's hosts pin is present on the server; the route stays down until the conduit is torn down.
Your LiteLLM proxy truncates long prompts in /spend/logs, but the
same calls are logged in full to Datadog. With these credentials, LiteLedger can query the Datadog Logs
Search API and backfill the untruncated prompt text onto synced requests. Credentials are shared across
your domain, encrypted at rest, and only a workspace admin can change them.
Match the DD_SITE your LiteLLM proxy logs to — it appears in your Datadog URL and Organization Settings.
Datadog → Organization Settings → API Keys. Sent as the DD-API-KEY header.
Datadog → Organization Settings → Application Keys (needs the logs_read_data permission). Sent as the DD-APPLICATION-KEY header.
When on, syncs may query Datadog to backfill truncated prompts, using both keys. Off, LiteLedger makes no Datadog calls and keeps the credentials.
Choose which AI provider runs each job. Workspace defaults apply to everyone who hasn't picked their own.
Choose which AI provider runs each job, or keep your workspace's default. Changes here are just for you.
Checking which Anthropic models your key can use…
{{ anthropicModelsNote }}
Models available on your key. Prices are per million input / output tokens.
Selectable once priced: {{ anthropicModelsUnpriced.join(', ') }}.
{{ workspaceModelsError }}
My models
✓ Saved
The model each step uses when you run Ask or an analysis.
{{ { analysis: 'Customer analysis', synthesis: 'Cohort report', ask: 'Ask' }[workloadPurposeForStage(stage)] || 'Customer analysis' }}
{{ providerLabel(workloadProviderForStage(stage)) }}
{{ workloadModeForStage(stage) === 'single' ? 'one model for every step' : 'a model per step' }}
↳{{ stage.label }}
Default: {{ workloadDefaultModel(stage) }}
{{ workloadEffectiveModel(stage) || 'Not set' }}
Checking which Anthropic models your key can use…
{{ anthropicModelsNote }}
Models available on your key. Prices are per million input / output tokens.
Selectable once priced: {{ anthropicModelsUnpriced.join(', ') }}.
{{ workloadModelsError }}
{{ g.label }}
You use {{ providerLabel(aiProviderTab) }} for{{ role }}You're not using {{ providerLabel(aiProviderTab) }} for any job yet.
A local LM Studio server on your own GPU, with no per-token cost.
{{ settings.local_inference_url }}
No servers found on port 1234.
{{ server.label }}{{ server.url }}Tailscale
{{ m }}
No models loaded
Rig cost rateUSD per million tokens · leave 0 to use cloud-equivalent estimate · excludes RunPod worker-time billing
Model Configuration✓ Saved
The model for each step. A step with no model uses the cloud default.
Local AI features currently blocked: · {{ f.name }} (missing {{ f.missing.join(', ') }})
No models match this filter. {{ settings.local_inference_url ? 'Load a model in LM Studio or switch to "All".' : 'Switch to "All" to see all recommendations.' }}
Set workers to min 0, max 1, and pin the model and container versions. Before sending customer data, send a unique test prompt and confirm it doesn't appear in the endpoint's logs or worker history.
Used for every step. Must match OPENAI_SERVED_MODEL_NAME_OVERRIDE on your endpoint.
Personal billing owner
{{ runpodBillingSetupLabel(billingScope) }}
Review the billing owner, record consent, then enable one bill import for this endpoint. Run allocation requires a separate reviewed policy.
{{ runpodCanonicalBillingLabel(billingScope) }}
{{ runpodAllocationSetupLabel(billingScope) }}
Checking retained allocation evidence…
Allocation evidence is not ready. A platform admin needs to verify the worker configuration, billing evidence and job coverage. Run costs stay pending.
Used for every step. Must match OPENAI_SERVED_MODEL_NAME_OVERRIDE on the endpoint.
Workspace billing owner
{{ runpodBillingSetupLabel(billingScope) }}
Review the billing owner, record consent, then enable one bill import for this endpoint. Run allocation requires a separate reviewed policy.
Existing legacy workspace imports can be turned off. Enabling the endpoint bill stops duplicate legacy imports and retains their history separately.
{{ runpodCanonicalBillingLabel(billingScope) }}
{{ runpodAllocationSetupLabel(billingScope) }}
Checking retained allocation evidence…
Allocation evidence is not ready. A platform admin needs to verify the worker configuration, billing evidence and job coverage. Run costs stay pending.
Costs use Google's published rates, including cached-input discounts.
Your Gemini API key (optional)
✓ SavedUsing your workspace's key
Loading available models from the Gemini API…{{ geminiModelsError }} You can type a model ID instead.Used for every step. If you don't save one, you get the workspace's model.
The timezone every date and time is shown in across LiteLedger — conversation history, activity charts, and logs.
Applies to your account only; leave on the workspace default ({{ settings.workspace_timezone || 'UTC' }}) to follow whatever your admin sets.
✓ Saved{{ userTimezoneError }}
Automatic Analysis
{{ askAutoAnalysis ? 'On' : 'Ask every time' }}
When an answer in Ask needs a customer analysis that is missing or out of date, LiteLedger runs it
and finishes the answer for you — up to {{ settings.ask_auto_analysis_max_customers }} customers
at a time. An answer that needs more than that goes ahead with the evidence available and says
where it falls short; when your workspace keeps analyses current in the background, those
customers are refreshed next. Applies to your account only, and you can stop a run while it is going.
Get a system notification when a task finishes while this tab is in the background. Choose which operations trigger one.
Browser notifications aren’t available here. You can still check finished tasks in the Activity menu.
Notifications are blocked. Open your browser's site settings for this URL and change Notifications to "Allow", then reload.
Your browser will ask for permission.
Fires a sample notification now, even with this tab in focus.
{{ testNotif.state === 'ok' ? '✓' : testNotif.state === 'pending' ? '·' : '✕' }} {{ testNotif.msg }}
{{ testNotif.hint }}
Inference activity
Every shared AI provider as analysis and Ask run: tokens, requests, spend and errors, updated live. The time window scopes the tiles, the chart and the request log; the provider chips filter the tiles and the chart.
{{ auditRunFilter ? 'Request log for one analysis run' : 'Request log' }}
The activity monitor stays workspace-wide. This table shows only the calls from one archived analysis run.
Loading…
{{ auditRunFilter ? 'No calls recorded for this run.' : ('No inference calls recorded in ' + auditWindowLabel.toLowerCase() + '.') }}
Showing the {{ auditLog.length.toLocaleString() }} most recent of {{ auditLogTotal.toLocaleString() }} calls in this window. Narrow the window or export for the full set.
Every provider and model seen in this workspace's inference log, and how its figures are priced. Token-priced providers use their registered rates, including cache rates. RunPod uses allocated shares of its endpoint's worker-time bill; tokens measure usage and throughput. Missing billing or allocation evidence keeps request costs pending.
Everything running or waiting across this workspace, with controls for the work you
started.
Analysis Queue
stale — retrying
{{ analyzeQueueLoaded ? aqRunning : '—' }} analyses started
{{ analyzeQueueLoaded ? aqQueued : '—' }} queued
{{ aqExternal }} outside the web app
Every running and queued analysis in this workspace, across all teammates.
Up to {{ aqLimit }} analyses per person; up to {{ aqHostLimit }} total across all workspaces on this server.
Each analysis makes multiple requests. Started analyses include work waiting for this shared request pool.
These request counts are from the last successful queue refresh.
Capacity could not be read. Request usage will appear after a successful refresh.
{{ aqRequestState.unresolved_remote }} unresolved requests still occupy slots until recovery verifies their status.
{{ syncOtherHold ? (syncStatus.lock_name || 'Another job') + ' in progress' : 'Sync in progress' }}Workspace data is being refreshed.
{{ aqError }}
Cancelling an analysis does not turn off the endpoint or guarantee that worker-time billing stops. Unconfirmed remote requests retain their slots until recovery verifies their status.
Can't read the analysis queue right now — retrying.
Loading the analysis queue…
The Customers page, the health summary and every customer row are served ready-made.
After a restart — or after a change that invalidates them — they are rebuilt here,
so the first person to open the page doesn't wait for it.
{{ warmingLabel }}{{ prepStepLine }}
about {{ fmtDur(prepStepEta) }} left in this step at the current rate
{{ fmtDur(prepElapsed || 0) }} / ~{{ fmtDur(prepExpectedTotal) }}
How long this takes is not known yet — {{ prepNoEstimateNote }}. This run's timings become that estimate.
{{ prepLastRunLine }}
This workspace's data has not been prepared yet.
A run like the last one typically takes ~{{ fmtDur(prepExpectedTotal) }} (from the last {{ prepStatus.history_count }} runs).
OAuth credentials are loaded from environment variables at startup
(single source of truth). To rotate them, edit
deploy.env on your dev box
or /etc/liteledger/secrets.env
on the production host, then restart the service.
Client ID
{{ settings.oauth_client_id }}not configured
Client Secret
●●●●●●●● setnot configured
Allowed Domain
{{ settings.oauth_allowed_domain }}any Google account
How to set up
Google Cloud Console → APIs & Services → Credentials → create an OAuth 2.0 Client ID (Web application).
Add your callback URL (https://<your-host>/auth/callback) as an authorized redirect URI.
Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and (optionally) GOOGLE_ALLOWED_DOMAIN in deploy.env.
Restart the service.
LDAP
ActiveInactive
LDAP settings are loaded from environment variables at startup
(single source of truth). To change them, edit
deploy.env on your dev box
or /etc/liteledger/secrets.env
on the production host, then restart the service. A user who signs in
with LDAP lands in the same account as their Google OAuth identity
when the email matches.
Server URL
{{ settings.ldap_url }}not configured
Base DN
{{ settings.ldap_base_dn }}not configured
User DN Template
{{ settings.ldap_user_dn_template }}default
Allowed Domain
{{ settings.ldap_allowed_domain }}any LDAP user
Timeout
{{ settings.ldap_timeout }}sdefault
How to set up
Set LDAP_ENABLED=1 in deploy.env (or the production secrets file).
Set LDAP_URL (e.g. ldaps://ipa.example.com:636) and LDAP_BASE_DN (e.g. cn=accounts,dc=example,dc=com). Both are required.
Optionally set LDAP_USER_DN_TEMPLATE (default uid={username},cn=users,{base_dn}), LDAP_ALLOWED_DOMAIN, and LDAP_TIMEOUT (default 10s).
Restart the service.
SMTP
ActiveInactive
Used for waitlist confirmations, pending-approval notifications, and
approval-granted emails. Admin overrides save to platform config and
take effect immediately. Leaving a field blank falls back to the env
file value (if any).
✓ Saved✗ {{ smtpSaveError }}
Send test message
Save a username and password before sending a test.
✓ Sent to {{ smtpTestResult.recipient }}· {{ smtpTestResult.elapsed_ms }} ms✗ {{ smtpTestResult.error }}
Email Templates
{{ emailTemplates.length }} templates
Every email this platform sends, rendered with sample data. Previews
come from the same code path as real sends, so what you see here is
what recipients get.
Loading…
No templates available.
Rendering…
Subject{{ emailTplDetail.subject }}
{{ emailTplDetail.source }}
{{ emailTplDetail.text }}
Failed to render preview.
Backups
Daily snapshots and backups taken before deployment. Verify reads the selected backup and checks its stored data; it can take several minutes. Database restore tests run separately as part of the health check.
Completed {{ absTz(backupVerifyResults[b.backup_id].details.completed_at) }}
· {{ backupVerifyResults[b.backup_id].details.duration_seconds.toFixed(1) }}s.
Database restore was not tested by this check.
✗ Verification failed:
{{ err }}
Server Health
Recorded once a minute on the server by the resource sampler, independent of this app —
history survives restarts, recycles, and deploys. A gap in a chart is a real gap in the record.
Times are in {{ displayTz ? displayTz.replace(/_/g, ' ') : 'your browser’s time zone' }}.
Could not load server health: {{ serverHealthError }}
Loading…
{{ serverHealth.reason }}
Last sample {{ shAge(shSampler.age_s) }} ago — the sampler may be stopped (check liteledger-metrics.timer).
No breach episodes in this window. A breach is {{ shBreachRule }}.
Sync Health
Every finished sync is compared with the runs before it. A problem that is still true is listed
under Open now and clears itself when a later run shows it fixed; one-off events stay in the log.
Could not load sync health: {{ syncHealthError }}
Loading…
{{ syncHealth.reason }}
Open critical
{{ syhOpenCount('critical') }}
Open warnings
{{ syhOpenCount('warning') }}
Recent runs flagged
{{ syhRunsFlagged }} of {{ (syncHealth.runs || []).length }}
Problems members flagged on an Ask answer, newest first. Each report captured the question, the answer, the sources it used and the model's own prompts at the moment it was filed — and while a report is open, those prompts are held back from the retention sweep so they are still here when you read it.
Status:Kind:
Loading reports…
No reports match these filters.
No answers have been reported.
{{ askReportKindLabel(r.kind) }}
{{ r.note || '(no description given)' }}
{{ r.reporter_name || r.reporter_email || 'unknown' }} ·
{{ relativeTime(r.created_at * 1000) }}
· no prompt trail (predates run correlation)
Model prompts
· {{ askReportDetail.calls.length }} call{{ askReportDetail.calls.length === 1 ? '' : 's' }}
· aged out of the retention window
· this answer predates prompt correlation
The prompts behind this answer were swept before the report was filed. Everything else in this report was captured at report time and is intact.
This answer was produced before Ask tagged its calls with a run id, so its prompts cannot be located. Answers reported from now on carry theirs.
⚠ {{ apiAuditFallbackCount.toLocaleString() }} audit row{{ apiAuditFallbackCount === 1 ? '' : 's' }} waiting in the fallback file — replayed into the database automatically once the write lock frees up.{{ apiAuditReplayNote }}
{{ apiAuditFallbackRejected.toLocaleString() }} row{{ apiAuditFallbackRejected === 1 ? '' : 's' }} could not be replayed and {{ apiAuditFallbackRejected === 1 ? 'is' : 'are' }} kept in audit_fallback.rejected.jsonl beside the database.
Mutation requests (POST, PUT, DELETE, PATCH) across every user in your workspace are recorded — syncs, analysis runs, settings changes, and other user-initiated actions. Read-only polling traffic is excluded. Each row captures method, path, status, duration, user email, and client IP; a row that can't reach the database while it is locked is held in audit_fallback.jsonl and replayed automatically.
User:
{{ apiAuditError }}
Loading…
No API requests recorded yet.
Time
Method
Path
Status
ms
User / IP
Error
{{ formatTime(row.ts) }}
{{ row.method }}
{{ row.path }}
{{ row.status }}
{{ row.duration_ms }}
{{ row.user_email }}anon{{ row.ip }}
{{ row.error || '' }}
Guides
Select a guide to read
How analysis reaches a report
Follow an analysis from the customer’s logged activity to a saved report. The path depends on the available evidence and the scope you choose.
Choose a customer and scope
Choose Recent, All available history, or a specific date range. The workspace default determines which report is preferred on the dashboard. Analysis Jobs shows queued and running work; the customer page shows the result.
Assemble and classify activity
Stage 0 groups the customer’s virtual keys and separates human turns from automated calls. Manual overrides and workspace rules apply before the default machine classification.
Read the available evidence
The full path discovers project context, annotates turns, and synthesizes sessions before writing the customer report. Sparse activity can use a minimal report. Machine-only activity follows a separate path and does not invent direct human sentiment.
Apply context and scoring
Enabled Primer knowledge gives the analysis product context. Server-owned scoring and postprocessing turn the stage outputs into the report’s displayed measures.
Save and inspect the report
Saved reports retain their scope and evidence window. Review the report’s depth and freshness labels before comparing customers. Cohort synthesis is a separate step over available customer reports.
Ask can investigate raw activity even when a customer has no saved analysis. The evidence it actually uses determines how you should read the answer.
Start with your question and context
Ask receives the conversation history, system instructions, enabled Primer context and relevant Primer excerpts. A recognized WebID can be resolved before the first model turn. Live customer activity is retrieved through tools.
Use the configured Ask provider
Ask has its own inference assignment. Provider capabilities determine caching and reasoning options. The web conversation and MCP Ask tool share the same answer loop.
Retrieve the evidence needed
Raw-data tools read usage, spend and conversations without requiring a report. Other tools read saved reports, analyzed annotations or Primer sources. These sources cover different populations and dates.
Follow up within the answer loop
The model can call tools, read their results and ask for more detail. The loop is bounded. A missing report is not evidence that a customer has no activity.
Read the answer with its coverage
The Coverage Ledger describes the evidence consulted and its scope. Raw activity can support usage findings; sentiment and health findings depend on the available analysis. Per-call detail uses the dates stored in LiteLedger, which may extend beyond the proxy’s re-fetch window.
{{ Object.keys(primerDraft.flags).length }} source{{ Object.keys(primerDraft.flags).length === 1 ? '' : 's' }} changed · Priority order changed. Ask, charts, and reports still use your saved settings.
Saving changes to analysis context may mark existing reports “Old primer”. Reports keep their content until you rerun them.
{{ primerDraft.error }}
Add knowledge Upload a document, paste text, or import a URL
or drop files here — Markdown, text, HTML, CSV, JSON, DOCX{{ primer.pdf_support ? ', PDF' : '' }}
(PDF support not installed on this server)
✗ {{ primerUploadError }}
Many documents? An AI assistant connected to LiteLedger can gather them from where they live, show you what it will add, and add them.
Connect an assistant, then ask it to load your Primer.
The page is fetched server-side and shown for review before anything is saved. Public pages, plain HTML/Markdown/text{{ primer.pdf_support ? '/PDF' : '' }} — content behind logins won't come through.
Document exceeds {{ fmtChars(primer.doc_max_chars) }} — the stored text is cut at that limit.
{{ primerPending.text }}
Digested when you add it into a summary, key facts and a glossary — one model call, usually under $0.01.
{{ primerStaleCount() }} document{{ primerStaleCount() === 1 ? '' : 's' }} without a digest
Without a digest, the always-on capsule carries a document’s opening text instead of its summary,
and its glossary terms are not defined for Ask or analyses. Digesting {{ primerStaleCount() === 1 ? 'it' : 'them' }} costs {{ primerDigestCostHint() }}.
{{ primer.digests_staled_by_refresh }} of {{ primer.digests_staled_by_refresh === 1 ? 'them had its' : 'them had their' }} source re-fetched on schedule since {{ primer.digests_staled_by_refresh === 1 ? 'it was' : 'they were' }} digested — nobody was here to rebuild {{ primer.digests_staled_by_refresh === 1 ? 'it' : 'them' }}.
New documents are digested automatically; these were added before that, or a run was interrupted.
{{ primerImportSummary(imp) }} from an AI assistant by {{ imp.applied_by || 'a workspace admin' }} · {{ fmtAgo(imp.applied_at) }}
{{ title }}
{{ primerImportNote }}
Change history {{ primerHistory.count }}
{{ primerHistory.legacy_marked }} change{{ primerHistory.legacy_marked === 1 ? '' : 's' }} from before this history began still mark{{ primerHistory.legacy_marked === 1 ? 's' : '' }} earlier reports Old primer, as the old rule did. Review {{ primerHistory.legacy_marked === 1 ? 'it' : 'them' }} to keep those reports current or leave them marked.
{{ primerHistory.unreviewed }} change{{ primerHistory.unreviewed === 1 ? ' has' : 's have' }} no decision yet; reports stay current until one is made.
Each change to what analyses read, and whether it marked existing reports Old primer.
Choose which sources Ask and analysis use. Changes to usage and priority apply when you save.
{{ primerKindFilter === 'campaign' ? 'Campaign briefs and the moments they put in context.' : primerKindFilter === 'document' ? 'The source of truth for your product.' : primerKindFilter === 'release_notes' ? 'What changed in your product and when.' : 'Product knowledge, campaign context, and release notes, in source priority order.' }}
{{ primerVisibleDocs().length }} of {{ primer.docs.length }} sources · Clear filters to reorder
No matching knowledge. Try another search or show all knowledge.
Campaign window→ Ongoingto One day{{ doc.campaign.timezone || primer.timezone }}
Digest
{{ primerDigestSource(primerDetail) }} · {{ fmtAgo(primerDetail.distilled_at) }}
stale — text changed since this was built; raw excerpts are used until re-distilled
{{ primerPreview.capsule || 'No capsule — no enabled documents for this purpose.' }}
{{ primerPreview.evidence
? 'Matched ' + primerPreview.matched.length + ' document' + (primerPreview.matched.length === 1 ? '' : 's') + ' (' + primerPreview.matched.join(', ') + ') — ' + fmtChars(primerPreview.evidence.length) + ' of evidence would ride along.'
: 'No passages matched. The capsule still applies; only the per-request evidence tier found nothing for this text.' }}
{{ primerPreview.evidence }}
Distillation
Each document is digested once into a summary, key facts, and a codename glossary.
The always-on capsule carries the summaries of your first documents in priority order,
each question or analysis gets the glossary terms it uses, and the full text stays
available to Ask on demand.
{{ primerEstimateLabel() }}
Distilling changes what analyses read, so existing reports will be marked “Old primer”.
They keep their content until you rerun them — so distill first, then rerun, or the reruns
are marked again.
›Ask reads the compiled block with every question — feature names, plans, and limits in answers come from here, not from guesswork.
›Analyses carry it in the report-writing stages (per-customer reports and the cohort report), where knowing the product sharpens use-case reads and recommended actions, and a shorter version in the earlier stages. Every question and every customer's analysis also gets the glossary terms it uses.
›Editing documents flags existing reports "Old primer". They still describe their customers; the next analysis re-runs with the current Primer. Nothing is rewritten retroactively.
Known virtual key aliases across synced history, grouped by account ID. Includes historical keys; current availability is not tracked.
{{ a.alias }}Known from key sync · No captured usageCaptured usage: {{ a.first_seen }} → {{ a.last_seen }}
{{ customerAliasRecords === null ? (customerStatsLoading ? 'Loading known key aliases…' : 'The key inventory could not be loaded.') : 'No key aliases found in synced data.' }}
Aggregate spend history only. This customer's per-call conversation rows aged out of the
LiteLLM proxy's short retention window (or predate this workspace's first sync) before they could
be captured — spend totals and the activity chart come from the long-retention daily archive, but
there are no messages, logs, or analyzable conversations, and there never will be for this period.
Excluded from workspace analysis and Ask.
Unhide this customer to run a new analysis.
{{ sentimentJobMine ? 'New analysis running' : 'Analysis running' }}— report will refresh automatically when complete· started by {{ ownerLabel(sentimentJobOwner) }}
{{ sentimentJobMine ? 'New analysis queued' : 'Analysis queued' }}— waiting for a worker slot· started by {{ ownerLabel(sentimentJobOwner) }}
Analysis running outside the web app— started by a script on the server; the report will refresh when it finishes· as {{ ownerLabel(sentimentExternalRun.owner) }}
Analysis improvements availableRun a new analysis to update this report.
{{ customerReport._meta.stale_classification_reason === 'new_data' ? 'New activity synced since this report' : 'Message classification has shifted since this report' }}
(Human: {{ customerReport._meta.cached_human_count }} → {{ customerReport._meta.current_human_count }})
— {{ customerReport._meta.stale_classification_reason === 'new_data' ? 're-run to include latest data' : 'rules or overrides changed; re-run to apply' }}
{{ customerReport._meta.new_rows }} new request{{ customerReport._meta.new_rows === 1 ? '' : 's' }} since last analysis— re-run to include latest data
Updating analysis searchQuote and friction searches may be incomplete. We’ll retry automatically.
{{ reportEra.partial ? 'Spans a change in coverage' : 'Limited coverage' }}{{ reportEra.reason }}Re-running reads the same records.Part of this period predates it.
Only {{ sentimentJobOwner ? ownerLabel(sentimentJobOwner) : 'the person who started it' }} can cancel this run.A new analysis can start when the server's run finishes.
✓Your existing report is up to date.
Last analyzed {{ relativeTime(customerReport._meta.analyzed_at) }}
Recent · {{ humanizeWindowRange(customerReport._meta.window.start, customerReport._meta.window.end) }}
All available history
Selected dates · {{ humanizeWindowRange(customerReport._meta.window.start, customerReport._meta.window.end) }}
Limited customer conversation. There is not enough evidence to assess health or churn risk.
This report describes automated activity. Health and churn risk cannot be assessed.
Based on the latest {{ (customerReport._meta.input_summary.automated_sample_count || 0).toLocaleString() }} of {{ (customerReport._meta.input_summary.selected_machine_count || 0).toLocaleString() }} saved calls in this period.
Session details were not saved with this report.
Analysis in progress — results will appear automatically when complete.
Analysis queued — will start shortly.
No call detailNothing for an analysis to read.
No human turnsNo human turns were found and the live classification hasn't resolved,
so there's nothing for an analysis to read yet. It re-checks as new data syncs.
Create an analysis for this customer.
This page will update when the analysis finishes. Started by {{ ownerLabel(sentimentJobOwner) }}.
Choose the activity to review. Each completed analysis is saved in History.
Reconnecting to live stream… (run continues)
{{ analyzePhase.label }} · quiet {{ Math.round(sentimentPulseStall) }}ssignal lost · retrying{{ sentimentPostedReadout }}{{ analyzeEtaLabel }}model loading~{{ analyzeThroughput }} tok/sawaiting output
{{ sentimentQueued ? 'Queued — waiting for a worker slot…' : 'Connecting to progress stream…' }}
This flags the answer for a workspace admin, with the question, the sources it used and the model's own prompts attached.
{{ askReportKindDesc }}
Attached automatically: the question, this answer, the sources and coverage,
the model and timing, and the prompts behind it —
which stay readable for as long as this report is open.
{{ analyzeActiveByTier[name].running }} running
{{ analyzeActiveByTier[name].queued }} queued
{{ t.stale }} stale
⊘
{{ t.no_turns }} no call detail
{{ TIER_INFO[tierTip.name] ? TIER_INFO[tierTip.name].label : tierTip.name }} · {{ TIER_INFO[tierTip.name] ? TIER_INFO[tierTip.name].band : '' }} {{ TIER_INFO[tierTip.name] ? TIER_INFO[tierTip.name].desc : '' }}
{{ tierTip.t.analyzed.toLocaleString() }} of {{ (tierTip.t.total - tierTip.t.no_turns).toLocaleString() }} eligible {{ tierTip.name }}-tier customer{{ (tierTip.t.total - tierTip.t.no_turns) === 1 ? '' : 's' }} analyzed ({{ tierTip.t.no_turns.toLocaleString() }} with no call detail excluded).
{{ tierTip.t.stale }} analyzed {{ tierTip.name }}-tier customer{{ tierTip.t.stale === 1 ? '' : 's' }} {{ tierTip.t.stale === 1 ? 'was' : 'were' }} scored before the latest analysis methodology change. The reports still show, but re-run analysis to refresh them under the current pipeline.
{{ tierTip.t.no_turns.toLocaleString() }} of {{ tierTip.t.total.toLocaleString() }} {{ tierTip.name }}-tier customer{{ tierTip.t.no_turns === 1 ? ' has' : 's have' }} spend but no per-call content for an analysis to read — typically their conversation rows aged out of the LiteLLM proxy's short retention window, or predate this workspace's first sync. Excluded from this tier's analyzed / total count. Customers whose requests are all machine-generated are still counted — the pipeline can analyze those.
Customer details are updating. You can keep browsing the list.
The customers list isn't available right now.
{{ analyzeGapsError.message }} This page checks again every 20 seconds.
Preparing the customer list. This page updates automatically.
No customers found.
Sync usage data first, then return here to run analysis.
Session summaries are available for {{ analyzeData.session_evidence_count }} of {{ analyzeData.analyzed_count }} customer reports. Older reports may need a new analysis to include them.
Health Score Distribution
Churn Risk Breakdown
Report generationAI
{{ synthesisAnalyzed - (synthesisReport?.customers_synthesized||0) > 0
? `+${synthesisAnalyzed - synthesisReport.customers_synthesized} new analyses since last run`
: 'Stale' }}
Select a model for every pipeline stage in Settings to enable the cohort report.
Running…
Regenerate failed:
{{ synthesisRunError.replace(/^ERROR:\s*/, '') }}
— showing the last successful report.
Report History
prior cohort verdicts, archived on each regenerate
No prior syntheses archived yet — they accumulate here each time you regenerate.
Cohort report running— this view refreshes automatically when it completes
{{ line }}
› {{ synthesisStreamChunk }}
{{ synthesisPulseBar }} {{ synthesisPulseLabel }} · T+{{ synthesisPulseElapsed.toFixed(1) }}s
· waiting for first model output — {{ synthesisPulseStall.toFixed(0) }}s elapsed · no new output for {{ synthesisPulseStall.toFixed(0) }}s — waiting on model
AI is writing… {{ synthesisStreamChars.toLocaleString() }} chars{{ synthesisElapsed }}s
No friction data yet — run more analyses to populate this section.
Explore frustration and other signals already identified by customer analysis. Each message counts once per signal. Rates describe assessed messages; missing analysis stays unknown.
{{ customerLearning.error }}
Preparing the complete census of retained human messages and existing analysis. No new model analysis is being run.
Prepared {{ absTz(customerLearning.data.prepared_at) }} · Dates and daily buckets use {{ customerLearning.data.scope.timezone }}.
{{ customerLearning.num(customerLearning.data.assessed_turns) }} of {{ customerLearning.num(customerLearning.data.eligible_turns) }} retained human messages
{{ customerLearning.num(customerLearning.data.invalid_turns) }} have invalid evidence
{{ customerLearning.num(customerLearning.data.unattributed_turns) }} retained human messages have no customer identity. They remain in message totals and are excluded from customer counts.
There is no fully validated analysis for this scope yet. This does not mean customers experienced no frustration. Use the existing Analyze workflow on Customers to improve coverage.
{{ customerLearning.num(customerLearning.data.unindexed_reports) }} older reports have no reusable archived message evidence.
Assessment gaps
Reasons can overlap. The headline message counts do not overlap.
Track LiteLLM truncation markers and possible gaps in the evidence you can inspect. These warnings describe stored logs; they do not establish that the model received an incomplete prompt.
{{ truncationAudit.preparationMessage }}
Couldn’t load this audit: {{ truncationAudit.error }}
Content quality is not indexed yet. Run a normal sync to assess retained requests. No truncation rate is available.
All retained requests in this scope have been checked for markers.
{{ truncationAudit.num(truncationAudit.data.reported_dropped_chars) }}characters reported missingAcross fields and requests; {{ truncationAudit.num(truncationAudit.data.amount_unknown_requests) }} requests also contain markers without a readable count.
{{ truncationAudit.num(truncationAudit.data.affected_customers) }}customers with affected requestsSibling keys grouped as one customer.
{{ truncationAudit.money(truncationAudit.data.affected_spend) }}associated request spendCost of affected requests, counted once. This is not an estimate of wasted spend.
No requests have been assessed in this scope. A normal sync continues assessment; zero detected markers here does not mean complete logs.
No markers detected in assessed requests. Absence of a marker does not prove completeness.
Is the evidence gap growing?
Daily marker rate among assessed requests. An empty day has no measured rate.
A reused system prompt contributes once per request that references it. These character totals are not unique lost text, bytes, tokens, or a measure of lost intelligence.
Is the user’s request being truncated?
{{ truncationAudit.num(truncationAudit.data.user_request_impact.prompt_affected_requests) }} prompts have a detected warning. These categories locate the gap relative to the leading <user_request>…</user_request> section. Each prompt counts once, even with multiple markers.
{{ truncationAudit.num(truncationAudit.data.user_request_impact.request_section_truncated_requests) }} of {{ truncationAudit.num(truncationAudit.data.user_request_impact.prompt_affected_requests) }} prompts with warnings have a gap inside the request or a closing tag removed by truncation.
{{ truncationAudit.num(truncationAudit.data.user_request_impact.categories.after_request.requests) }} prompts retain both tags and the entire enclosed span before every marker. The detected gaps occur in content that follows.
{{ truncationAudit.num(truncationAudit.data.user_request_impact.categories.no_envelope.requests) }} prompts have no recognizable leading opening tag. Their warnings cannot be attributed to request text or appended content.
Among the {{ truncationAudit.num(truncationAudit.data.user_request_impact.enveloped_prompt_affected_requests) }} warning-bearing prompts with a recognizable opening tag, {{ truncationAudit.pct(truncationAudit.data.user_request_impact.request_section_truncated_fraction_of_enveloped) }} show truncation within the request section. Requests with warnings only in other fields are excluded. Every opening tag originally has a closing tag, so a missing close was truncated away. Missing-character totals cannot isolate how much request text was lost. These boundaries do not establish who authored the enclosed text.
{{ truncationAudit.num(truncationAudit.data.ambiguous_marker_requests) }} requests contain markers without the usual wrapper. Quoted markers can produce matches, including when the wrapper is present. All counts remain observations, not verified loss.
Where to investigate
Ranked by affected request count. Compare rates against each group’s own assessed traffic; model association does not establish cause.
Showing the first 20 groups. Headline totals cover the entire selected scope.
Inspect prepared evidence
Up to 20 matching examples from the {{ truncationAudit.data.example_daily_limit || 100 }} most recent affected requests collected per day. Filters and hidden customers can reduce this sample. Examples do not determine the totals. Inspect all affected logs →
{{ truncationAudit.data.affected_requests ? 'No prepared examples match this scope. Use the logs link to inspect affected requests.' : 'No affected requests to inspect.' }}
{{ truncationAudit.evidence[example.request_id].data[field] || 'No text stored.' }}
How to investigate and reduce the gaps
Inspect markers in stored fields. Check whether the warning is quoted, and whether the missing span intersects the customer message, system context, or response.
Compare affected models and customers over the same window. Use daily rates and assessment coverage to check whether an apparent change is a traffic shift or newly assessed history.
Match a request ID against a configured logging callback such as OTEL or Datadog. Verify that a complete copy was actually captured and retained; the warning alone does not prove recovery is possible.
Review the deployed LiteLLM version and MAX_STRING_LENGTH_PROMPT_IN_DB. Increasing the storage limit can reduce future truncation; it cannot restore text already omitted.
After a change, compare equivalent windows and inspect fresh requests. Reanalyzing the same incomplete text cannot recover missing evidence.
This audit checks markers in five retained fields. It does not cover every upstream payload, establish the exact database limit, measure model context-window truncation, or measure LiteLedger’s separate analysis-input limits. LiteLLM’s marker definitions ↗
Customers whose budget was exhausted during AI-builder intake, before a genuine request.
Loading intake audit
Computing the cohort... first load runs a one-time scan (~a minute).
Couldn’t load the audit: {{ intakeAudit.error }}
{{ intakeAuditStats.n.toLocaleString() }}
affected of {{ intakeAudit.summary.segment_count.toLocaleString() }} all-time
{{ intakeAuditWindowLabel }}
{{ intakeAuditStats.pctOfAll.toFixed(1) }}%
of {{ (intakeAuditStats.total||0).toLocaleString() }} customers
{{ (intakeTip.ts||'').slice(0,19).replace('T',' ') }} UTC
cost{{ intakeTipCost }}{{ intakeTipShare }}
model{{ intakeTip.model }}
tokens{{ intakeTipTokens }}
latency{{ intakeTipTiming }}
outcome{{ intakeTipOutcome }}
reading message…
Couldn’t load this turn’s text.
{{ intakeTipPreview(intakeTip.text) }}
(empty message)
Builder retries after a proposed file edit failed to apply. Each row shows one affected customer and their retry costs; strip colors identify the failure reasons.
Loading rejected-edit audit
Computing the cohort… first load runs a one-time scan.
Couldn’t load the audit: {{ editsAudit.error }}
{{ editsStats.events.toLocaleString() }}
retries
{{ editsWindowLabel }}
{{ editsStats.customers.toLocaleString() }}
customers affected
{{ editsWindowLabel }}
{{ editsStats.changes.toLocaleString() }}
files rejected
{{ editsWindowLabel }}
${{ editsStats.spend.toFixed(2) }}
retry tax ·{{ editsAudit.summary.pct_of_spend.toFixed(2) }}% of spend
{{ editsEventsView(r.key_alias).length }} retries, newest first — this text is the builder re-prompting itself, never the customer.
worst files: {{ p.path }} ×{{ p.changes }},
MCP lets Claude use LiteLedger tools. After connecting, you can ask for customer reports, spend data,
sentiment, analyses, or a data sync.
Choose the setup below for Claude Desktop, Claude Code, or another MCP client. Team members without
Owner access can use the Desktop extension. Team Owners can also configure a hosted connector.
In Claude Desktop, open Settings → Extensions → Advanced settings → Extension Developer → Install Extension…
and choose the downloaded file. If LiteLedger is already installed, use Update when available.
3
Enable the extension
On macOS, Claude may ask to access Claude Safe Storage. Confirm that the prompt names
Claude Helper (Plugin), then choose Always Allow.
4
Sign in with Google
When the extension starts, your browser opens the LiteLedger sign-in. Use the same Google account you
use for LiteLedger. The consent page identifies the application as LiteLedger Desktop.
To open the sign-in again, turn LiteLedger off and on in
Settings → Extensions.
5
Confirm the account
After the connection completes, ask:
"Which LiteLedger account am I connected to?"
Connect Claude Code
Use this option for Claude Code in a terminal. Sign in with the same Google account you use for
LiteLedger. No API key is required.
1
Add the server
Run this command in a terminal:
{{ mcpAddCommand }}
--scope user makes LiteLedger available in every project.
Omit it to add LiteLedger only to the current project. Remove it later with
claude mcp remove liteledger --scope user.
2
Authenticate with Google
The first LiteLedger request opens Google sign-in in your browser. Use the same account you use for
LiteLedger and complete the consent screen.
3
Check that it worked
Start Claude Code and ask it:
"Which LiteLedger account am I connected to?"
The response should show the same email address and workspace you use in LiteLedger.
◆
Hosted connector (Owner setup)
Use this option for Claude Desktop, web, and mobile when a Team or Enterprise Owner can add LiteLedger
to the organization. Team members cannot create a hosted connector themselves.
1
Open Claude's connector settings
On Free, Pro, or Max, go to Customize → Connectors → Browse, then choose Add custom connector (older builds show a + instead).
On Team or Enterprise, an Owner first adds it at Organization settings → Connectors → Add → Custom → Web;
members then find LiteLedger under Customize → Connectors.
2
Add LiteLedger
Enter LiteLedger as the name and paste this URL. Leave the optional OAuth Client ID and secret fields empty.
{{ mcpEndpoint }}
3
Connect your account
Click Connect, sign in with the same Google account you use for LiteLedger, and allow access.
4
Confirm it worked
In a Claude chat, use the + menu to enable LiteLedger, then ask:
"Which LiteLedger account am I connected to?"
In Claude Desktop, use either the extension or the hosted connector, not both.
Any other MCP client
For a client that supports remote MCP over Streamable HTTP, add this server URL and sign in with Google:
{{ mcpEndpoint }}
Protocol: Streamable HTTP. Authentication: Google OAuth with dynamic client registration and PKCE.
The Desktop extension is only for Claude Desktop. Other clients should connect directly to the remote URL.
How the connection works
Claude receives descriptions of LiteLedger tools after the connection is enabled.
Tool use
Claude selects a LiteLedger tool when it matches your request. Name LiteLedger in the prompt when you
want Claude to use it explicitly.
Request data
When a tool runs, LiteLedger receives the tool arguments and your authenticated account. It does not
receive the rest of the Claude conversation.
Authentication
Google sign-in connects Claude to your LiteLedger workspace and applies your existing permissions. The
requested scopes are your email address and basic profile.
Connection path
The Claude Desktop extension forwards requests to
{{ mcpEndpoint }}. Hosted connectors and Claude Code connect
to the same endpoint directly. LiteLedger data and reports remain on the LiteLedger server.
The complete tool set
Available tools, grouped by function. Claude selects these tools based on your request.
Reads — instant
whoamiWhich account / workspace this connection resolves to.
get_workspace_overviewDashboard snapshot across all customers — lifetime & recent cost to serve, request and customer counts, plus interaction and reliability rollups.
list_customers(tier?, segment?, analyzed_only?)List customers by engagement tier, segment, or analysis status.
get_customer_report(alias, scope?, report_id?)One customer's sentiment report — health, churn risk, what's going well, what's causing friction, and what to do about it. You get the same one the dashboard shows unless you ask for a particular version.
list_customer_analyses(alias)Every analysis that exists for one customer — each slice (recent, full, focused windows) with its report ids and version history.
get_customer_sessions(alias)The conversation sessions analyzed for one customer.
get_customer_interactions(alias)Interaction rollup — each human turn plus its downstream machine calls.
get_customer_turns(alias, limit?, signals_only?, offset?, order?)Raw classified turn-by-turn data; order accepts newest (default) or oldest, and signals_only keeps only sentiment-bearing turns.
get_cohort_synthesisA written synthesis of patterns across analyzed customers.
get_cohort_summaryThe same population as numbers — health histogram, churn counts, archetype mix, top pains, and the CS outreach queue.
get_daily_spend(days?)Cost-to-serve trend — daily spend / requests / tokens across all customers over the last N days.
get_spend_by_modelSpend, requests, and tokens broken down by model.
get_top_customers(limit?)Which customers cost the most to serve, ranked.
get_budget_statusBudget utilization across keys (available / warning / exceeded).
get_segment_breakdownYour whole customer base split by segment — how many customers and how much spend sits in each.
Search & signal mining
search_conversations(query, …)Full-text search across turns; scope to a customer, a date window, by relevance or recency.
find_quotes(topic?, sentiment?, signal_type?, …)Representative customer quotes, filterable by topic, sentiment, and signal type.
get_friction_topics(signal?, key_alias?, …)Cluster friction signals (default frustration) into topics.
Ask — agentic
ask(question, history?)Run a multi-step question through LiteLedger's analyst. Use direct lookup tools for single facts.
Actions — trigger then poll
run_analysis(key_alias, scope?, force?, …)Start a server-side sentiment analysis (server does the inference, on the workspace's budget). Returns a handle immediately. 2–15 min.
analysis_status(key_alias, scope?)Return the current status of a server-side analysis.
trigger_sync(mode?)Fetch data from LiteLLM. auto is the standard mode; full re-fetches all data. Usually takes 5–30 minutes.
sync_statusLast-sync metadata, whether one's running, DB stats, staleness, and a preview of the next sync.
Client-side analysis — the ④ machinery
Claude uses these tools in sequence for client-side analysis.
fetch_framework(version?)The version-locked Stage 1–3 prompts, output schemas, and thresholds. Analyze strictly against this.
fetch_analysis_input(key_alias, scope?, …)Stage-0 output for one customer: human turns, batch units for fan-out, a pre-allocated report id, and a drift fingerprint.
submit_analysis(key_alias, report_id, …)Submit your finished report. The server re-scores, validates, stamps provenance, and stores it.
How to use each feature
Example prompts for common LiteLedger tasks.
① Ask questions about your customers
Ask for customer status, reports, spend, sentiment, friction topics, or quotes.
Direct lookups return stored data. Broader questions use LiteLedger's analyst and consume workspace
AI budget.
Broader questions require an AI provider under Settings.
Direct lookups do not.
Direct lookups
"Which power-tier customers have never been analyzed?""Pull the latest report for account-1000012-customer and list its top pain points.""What are the top frustration topics across all customers in the last 14 days?""Show me quotes where customers were delighted this month."
Broader questions
"Which customers should customer success reach out to this week, and what should they say?""What's the most common reason customers give up mid-task? Quote the moments they did.""What are customers asking for that we don't support yet?""Of the customers most expensive to serve, which are succeeding and which are just retrying?"
Customer identifiers. Use the full key name
(account-1000012-customer) or the account number
(1000012). Ask for the customer list if you do not know the identifier.
Spend. Spend, token, and request totals are
cost-to-serve metrics, not customer revenue.
② Run analysis on LiteLedger servers
Include on your servers in the request to run analysis with the workspace AI budget. A
server-side analysis usually takes 2–15 minutes and continues after the chat closes.
Use server-side analysis for long customer histories or when the chat cannot remain open.
The default scope is recent activity. You can request full history or a specific date range. Claude
asks for confirmation before rerunning an analysis that is already current.
"Analyze account-1000012-customer on your servers and tell me when the report's ready.""Analyze 1000012 over just the last 30 days." # focused window"Give me a deep dive on 1000012's whole history, not just the recent window." # scope=full"On your servers, analyze the five highest-spend customers we've never analyzed, then tell me which one looks worst."
Each customer analysis uses workspace AI budget. Specify how many customers to analyze.
③ Fetch the latest data
LiteLedger tools read data from the latest completed sync. A new sync usually takes 5–30 minutes.
Only one sync runs per workspace at a time.
"How fresh is the data — when did we last sync?""Sync, then tell me which customers have been active since yesterday."
◆④ Run analysis in your Claude session (default)
By default, Claude runs the analysis in the current chat using your Claude subscription instead of
the workspace AI budget.
Keep the chat open until the report is submitted. For a detached job, request analysis
on your servers.
Claude prepares the analysis. LiteLedger validates it, calculates health and churn scores, and stores
the report.
The saved report appears in the customer's history with an
◆ MCP label.
"Analyze account-1000012-customer."
Claude fetches the current analysis instructions before it starts. In Claude Code, you can also use
/mcp__liteledger__analyze_customer.
For long customer histories, use a date range or request server-side analysis.
Good to know
•Reports and syncs use the same LiteLedger workspace shown on this site.
•Read tools use data from the latest completed sync.
•Analysis in your Claude session requires the chat to remain open. Server-side analysis and sync jobs continue after the chat closes.
•Ask for an existing report when you do not need a new analysis.
•Questions asked through MCP are stored in the connected app, not in LiteLedger's Ask history.
Version history
Changes to LiteLedger's MCP server and the Claude Desktop extension, newest first. Server changes apply to
every connected app automatically. A new extension version is installed from the download above.
Nov 3, 2026
A Soft launch customer segment
•Segment breakdowns and the customer list's segment filter now include Soft launch: customers first seen before the public launch, on the starter budget. They were counted as unclassified before. Workspaces with segments turned off are unchanged.
•Try: “How does spend from soft-launch customers compare with GTM customers?”
Oct 29, 2026
Export customer signals to combine with other sources
•Export every customer's daily, weekly or monthly signal counts for an exact UTC date range, or page through every signal-bearing message with source quotes. Each export includes assessment gaps, stable record IDs and complete totals for checking that every page arrived.
•Account IDs and retained project tags support correlation with other customer data. LiteLLM trace IDs do not identify product sessions; plan tiers and product-session links remain unavailable. Reading the export reuses existing analysis.
•Try: “Export all weekly customer signals and message evidence for September 28 through October 25, 2026, inclusive UTC, for our customer frustration index. Retrieve every page and reconcile the totals.”
Oct 29, 2026
Customer signals with evidence and coverage
•Ask your assistant about frustration, confusion, success or delight already identified in customer analysis. Results include exact message counts, assessment coverage and source quotes. Missing analysis stays unassessed, and reading the report makes no new analysis calls. Large reports prepare in the background; your assistant can check again while preparation is pending.
Oct 1, 2026
A lighter start to every conversation
•The tool list and standing instructions your assistant loads at the start of every conversation are more than a fifth smaller. The tools and what they return are unchanged.
Sep 30, 2026
Primer: summary and glossary
•The always-on summary holds the first documents in your priority order, each with the same room, so a long title or a long digest no longer pushes another document out. A document still waiting for its digest carries its opening text.
•Glossary terms are defined wherever a question or a customer conversation uses them, instead of in a short fixed list.
•When you view a whole staged import, every entry shows the check of what arrived.
Sep 29, 2026
Primer loading: long documents, layout and digests
•Your assistant can send a long document in parts and check that every character arrived. Text that arrives different from what was sent is not saved.
•Documents keep their layout: indentation, tables, code and diagrams are saved as written.
•A document can be taken out of a staged import before it is applied, and an import you decide against can be discarded.
•Your assistant can set a document's place in the priority order. The always-on summary gives each document it includes a readable digest, and says how far down the list it reaches.
•Documents with long glossaries now digest. A digest that fails says why, and your assistant can digest every document still without one.
Sep 29, 2026
Load your Primer from your AI assistant
•Ask your assistant to load your Primer. It gathers your product documents from the places it can reach, or public pages by their address, and shows you what it will add before anything is saved.
•It can update a document that is already there. The replaced text stays in that document's history, and one request undoes a whole import.
•Your assistant can read every Primer document and see which ones reach Ask and analyses. Workspace admins can add and change documents; everyone else can read them.
•In apps that list server prompts, Load Primer starts it for you.
Sep 25, 2026
Syncs over a VPN route carry over what they don't reach
•A sync over a VPN route downloads for at most 2½ minutes. Days it doesn't reach are carried over, and the next sync starts as soon as it ends and fetches them first, so a full data sync finishes over a run of ordinary syncs.
•Sync status reports how many days are carried over and whether the next sync is already on its way.
Sep 24, 2026Desktop extension 1.1.0
Sign-in on Claude Desktop's default settings
•When you enable the extension, your browser opens the Google sign-in. Approve LiteLedger Desktop, and LiteLedger's tools appear in Claude.
•The "Connect LiteLedger" step is gone.
•Versions 1.0.2 through 1.0.5 couldn't start their sign-in on the Node.js built into Claude Desktop, which it uses by default. Install 1.1.0 over any of them.
Sep 24, 2026Desktop extension 1.0.5
Sign-in problems explained
•When the sign-in can't start, the extension says why instead of reporting that a page is opening.
Sep 24, 2026Desktop extension 1.0.4
Connecting after sign-in
•After you sign in, the extension switches to LiteLedger's tools by itself.
•Its bundled components are updated to patched versions.
Sep 24, 2026
Prompt errors
•The analyze_customer prompt reports an unexpected error the way tools do: a short message with a reference code.
Sep 22, 2026
Sign-in, errors and tool names
•Signing in requires a Google account with a verified email address.
•When a tool fails unexpectedly, you get a short message with a reference code to quote if you report it.
•trigger_sync offers auto, incremental and full syncs, and its test mode is gone. A workspace can start one sync every 30 seconds.
•Each tool has a readable name, which permission prompts show.
•Tool descriptions are shorter, so LiteLedger uses less of each conversation's context.
•Connection details show LiteLedger's release instead of the MCP framework's version.
Sep 18, 2026
Starting a sync
•Only workspace admins can start a sync with trigger_sync.
•Before a sync starts, LiteLedger checks that your LiteLLM proxy answers. If it doesn't, nothing runs and Sync History records why.
•A sync you start uses your own VPN route, if you have set one up under Settings → AI & Models.
Sep 17, 2026
Content completeness
•New get_content_quality tool: exact counts of stored text that was cut short, and how many characters were lost.
•With require_complete_content, get_customer_report and get_customer_sessions treat shortened source text as blocking instead of as a caveat.
Aug 21, 2026Desktop extension 1.0.3
Sign-in on request
•Sign-in starts when you ask Claude to connect LiteLedger, not when the extension starts.
Aug 21, 2026Desktop extension 1.0.2
One sign-in for several copies
•When Claude Desktop starts more than one copy of the extension, they share one sign-in.
Aug 18, 2026
Edits Not Applied report
•New get_edits_not_applied_audit and get_edits_customer_events tools read the Edits Not Applied report.
Aug 17, 2026
Analysis types
•The Quick report types are retired. run_analysis runs a recent (the default), full or focused analysis.
Aug 12, 2026
Segments and product knowledge
•list_customers returns each customer's segment and can filter by it. The new get_segment_breakdown totals customers, spend and requests per segment.
•Reports say when they were written with older product knowledge than your workspace has now.
•A client-side analysis still saves when product knowledge changes while it runs, and the report is marked as written with the earlier knowledge.
Aug 10, 2026
Staying connected
•Connections keep working through LiteLedger updates and restarts, with no reconnect.
•New get_intake_budget_audit and get_intake_customer_turns tools read the Budget Exhausted at Intake report.
Aug 8, 2026
Spend per key
•New get_customer_key_breakdown tool: spend and requests for each virtual key within a customer.
Aug 6, 2026Desktop extension 1.0.1
Extension name
•The extension appears as LiteLedger on every Claude Desktop screen. If you installed 1.0.0, remove it before installing a later version.
Aug 6, 2026
Client-side analysis by default
•Analyzing a customer runs in your own Claude session by default, on your own subscription, and the report is stored like any other. Ask for the analysis on your servers to use run_analysis instead.
•Client-side analysis works in any MCP client: LiteLedger serves the instructions, and the analyze_customer prompt starts a run.
•list_customers returns 100 customers per page by default, up to 1,000, and answers in seconds on large workspaces.
•get_customer_turns can return the oldest turns first.
Aug 5, 2026Desktop extension 1.0.0
Claude Desktop extension
•Install LiteLedger in Claude Desktop from one file, with no Node.js to install and no configuration file to edit.
Aug 5, 2026
Fewer permission prompts
•Read-only tools no longer ask for permission each time. run_analysis, trigger_sync and submit_analysis, which change data, still ask.
Aug 3, 2026
Paging through turns
•get_customer_turns pages through a customer's whole history, up to 100 turns at a time, and says whether more remain.
Jul 15, 2026
Product Knowledge
•ask and analyses use the product documentation your workspace adds to Product Knowledge.
Jul 9, 2026
Customers with several keys
•Virtual keys that share an account id count as one customer in every tool.
Jul 6, 2026
Finding customers
•Customer tools accept a bare account id, such as 2974700. An unknown customer gets an error listing close matches instead of an empty result.
•A workspace that hasn't synced yet gets a plain message instead of a database error.
Jun 26, 2026
Every analysis type
•The new list_customer_analyses lists every analysis of a customer, and get_customer_report reads a specific type or version.
Jun 9, 2026
Recent activity by default
•Analyses, client-side ones included, cover each customer's recent activity by default: the latest 14 days of activity unless your workspace sets another window. Full history is an explicit option.
Jun 3, 2026
LiteLedger MCP launches
•Connect an MCP client with Google sign-in and use your LiteLedger workspace from it.
•Read tools cover customers, reports, sessions, turns, spend, budgets, quotes and friction topics. ask answers questions that need several of them.
•run_analysis and trigger_sync start longer jobs, which you follow with analysis_status and sync_status.
•fetch_framework, fetch_analysis_input and submit_analysis let your own Claude session run a customer analysis.