Summary
The Call detail modal's PROVIDERS row shows the same provider more than once, plus an empty chip. Reported on Discord with a screenshot showing six chips for one call:
— Cartesia Deepgram Gemini cartesia deepgram google
That is three real providers rendered as six chips, plus one blank.
Not a rendering bug
Sessions.tsx:358 maps the array one-to-one with no transformation:
session.providers.map((p) => (
<span key={p} className="neo-badge neo-badge--black">{p}</span>
))
So the duplicates are in the data. The read is a raw passthrough too, repository/session_repository.py:143:
SELECT DISTINCT provider FROM requests
WHERE session_id = :session_id ORDER BY provider
DISTINCT is case-sensitive on TEXT, so two spellings of one provider survive as two rows.
Three distinct causes
1. Empty chip: EOU rows write an empty provider.
inference/session/capture.py:681 writes end-of-utterance timing rows with provider="" and model_id="". replay_capture_middleware.py:170 does the same for state snapshots. The DISTINCT query has no WHERE provider != '', so the empty string comes back and renders as a blank badge.
This matches the screenshot exactly: the same call shows EOU | 8 requests | $0.000000 in the per-modality breakdown.
2. Case duplicates: the pipecat fallback never lowercases, despite saying it does.
inference/pipecat/observer.py:100-121. The docstring states it will "lowercase the leading alpha run as a weak provider hint". The code does not:
name = getattr(processor, "name", "") or ""
# Processor names look like "OpenAILLMService#0"; strip the "#N" suffix and
# lowercase the leading alpha run as a weak provider hint.
head = name.split("#", 1)[0]
return head or ""
head is returned verbatim, so a processor named CartesiaTTSService#0 yields CartesiaTTSService, and any branded name yields a capitalized provider. Meanwhile the primary path returns the module segment (pipecat.services.cartesia.tts to cartesia), lowercase. Both write the same column.
session/capture.py:229-231 has the same shape of gap: it falls back to getattr(component, "provider"/"_provider") and returns whatever the plugin exposes, with no normalization.
3. Alias duplicates: Gemini and google are the same provider.
The LiveKit module segment gives google (livekit.plugins.google). Something else reports Gemini. There is no alias map anywhere, so both persist as separate providers. This one is not fixed by lowercasing.
Impact
Cosmetic in the modal, but the provider column is not only used for display. Anything that groups or filters by provider (rate rules, per-provider cost rollups, reconciliation) sees Cartesia and cartesia as two providers, so per-provider totals split across spellings.
Suggested fix
Normalize at the write site, not the read site, so already-stored rows and future rows agree:
- Lowercase provider in both derivations (
capture.py:_provider_name, observer.py:_provider_from_module), which is what the pipecat docstring already promises.
- Add a small alias map for branded names to canonical ids (
gemini to google, and whatever else turns up).
- Filter empties out of the read:
WHERE provider != '', or stop writing provider="" on EOU/state rows and use a sentinel the read excludes.
- A migration or one-off normalization pass for existing rows, otherwise old calls keep showing both.
Worth a test asserting that a session whose requests contain mixed spellings returns a single chip per provider.
Reproduce
Run an agent with the pipecat path where a service falls back to the name derivation, or any session mixing a module-derived provider with an attribute-derived one, then open Call detail.
Summary
The Call detail modal's PROVIDERS row shows the same provider more than once, plus an empty chip. Reported on Discord with a screenshot showing six chips for one call:
—CartesiaDeepgramGeminicartesiadeepgramgoogleThat is three real providers rendered as six chips, plus one blank.
Not a rendering bug
Sessions.tsx:358maps the array one-to-one with no transformation:So the duplicates are in the data. The read is a raw passthrough too,
repository/session_repository.py:143:DISTINCTis case-sensitive on TEXT, so two spellings of one provider survive as two rows.Three distinct causes
1. Empty chip: EOU rows write an empty provider.
inference/session/capture.py:681writes end-of-utterance timing rows withprovider=""andmodel_id="".replay_capture_middleware.py:170does the same for state snapshots. TheDISTINCTquery has noWHERE provider != '', so the empty string comes back and renders as a blank badge.This matches the screenshot exactly: the same call shows
EOU | 8 requests | $0.000000in the per-modality breakdown.2. Case duplicates: the pipecat fallback never lowercases, despite saying it does.
inference/pipecat/observer.py:100-121. The docstring states it will "lowercase the leading alpha run as a weak provider hint". The code does not:headis returned verbatim, so a processor namedCartesiaTTSService#0yieldsCartesiaTTSService, and any brandednameyields a capitalized provider. Meanwhile the primary path returns the module segment (pipecat.services.cartesia.ttstocartesia), lowercase. Both write the same column.session/capture.py:229-231has the same shape of gap: it falls back togetattr(component, "provider"/"_provider")and returns whatever the plugin exposes, with no normalization.3. Alias duplicates:
Geminiandgoogleare the same provider.The LiveKit module segment gives
google(livekit.plugins.google). Something else reportsGemini. There is no alias map anywhere, so both persist as separate providers. This one is not fixed by lowercasing.Impact
Cosmetic in the modal, but the
providercolumn is not only used for display. Anything that groups or filters by provider (rate rules, per-provider cost rollups, reconciliation) seesCartesiaandcartesiaas two providers, so per-provider totals split across spellings.Suggested fix
Normalize at the write site, not the read site, so already-stored rows and future rows agree:
capture.py:_provider_name,observer.py:_provider_from_module), which is what the pipecat docstring already promises.geminitogoogle, and whatever else turns up).WHERE provider != '', or stop writingprovider=""on EOU/state rows and use a sentinel the read excludes.Worth a test asserting that a session whose requests contain mixed spellings returns a single chip per provider.
Reproduce
Run an agent with the pipecat path where a service falls back to the
namederivation, or any session mixing a module-derived provider with an attribute-derived one, then open Call detail.