Org-wide AI consumption¶
Every AI call ConnectGain makes on an organization's behalf now writes one priced row to ai_usage_events, tagged with the feature that spent it. The billing page turns that ledger into one question an owner can actually answer: what is my AI bill, and which product surface is spending it?
This extends the bot-flow metering model to Call Intelligence and Meetings, and adds a global page that puts all of it side by side.
- Route:
/billing/ai-usage(linked from/billingand the Admin group in the sidebar) - Access: OWNER / ADMIN only
- Periods: Day / Week (ISO week, UTC) / Month — the same windows as
/flows/ai-usage, so the two pages reconcile
The feature dimension¶
ai_usage_events.feature is the column everything hangs off:
| Feature | Written by | Metered stages |
|---|---|---|
bot_flows |
gemini-proxy, gemini-meter, flow-agent-respond, n8n-flow-callback, the platform, the platform, the platform |
one row per Gemini call (incl. each agent tool-loop iteration), smart-query intent extraction, product search embeddings |
call_intelligence |
process-call-recording, suggest-call-scorecard |
transcription, analysis, analysis_repair |
meetings |
process-call-recording, generate-meeting-minutes |
transcription, meeting_minutes (incl. regenerations) |
voice_agent |
voice-agent-session → voice_agent_usage + post-call voice_analysis |
one row per completed Gemini Live call, plus transcript analysis |
inbox_ai |
ai-agent-respond, ai-classify-intent, ai-internal-assistant, ai-web-assistant, analyze-conversation, summarize-conversation, generate-reply-suggestions, extract-faqs, the reply-assistant KB importers, speech-to-text |
one row per call |
reengagement |
ai-generate-reengagement, ai-generate-sequence-content, generate-upsell-drafts |
outbound message generation |
other |
generate-product-embeddings, qdrant-ingest-files |
catalog embeddings, document text extraction |
The stage is stored in node_type (bot flows already used that column for the node kind), and metadata.stage repeats it alongside the vendor.
How coverage is kept complete¶
Almost every Gemini-backed feature reaches the API through callGemini / callChat in
the platform, and both meter centrally. Those helpers always took a fine-grained
AiFeature argument (agent_respond, summarize_conversation, …) and previously ignored it;
billingFeatureFor now maps it onto a billing line, and the fine-grained name is kept as the
event's node_type so the drill-down stays exact.
That means a new AI feature is metered by default — the cost shows up on the billing page
without anyone remembering to wire it up. The inverse is the thing to watch: a caller that writes
its own ai_usage_events row must pass skipMetering: true or it will be billed twice.
flow-agent-respond is the one such caller today (it meters per flow node, carrying the
bot_flow_id).
A handful of call sites reach Gemini directly instead of through the helpers and are metered
inline: speech-to-text, the platform, the platform
(smart-query intent), the platform, generate-product-embeddings,
qdrant-ingest-files, and the call/meeting pipeline.
Deliberately not metered¶
These are excluded on purpose, not by omission:
| Not metered | Why |
|---|---|
translate-message |
Uses the MyMemory translation API, not an LLM — no tokens are billed |
auto-tag-contacts |
Rule-based tagging; makes no model call at all |
enrich-contacts |
Calls data vendors (Apollo, Prospeo, Snov, Clearbit), not an LLM |
| the platform | Documentation embeddings run on Appgain's key (DOCS_EMBEDDING_ORG_ID), so they are platform cost, not the customer's |
| Cloudflare Workers AI embeddings | A different provider and account; not part of the Gemini bill |
test-ai-vendor |
A fixed ~20-token connection probe, fired only when an admin clicks "Test" |
Embeddings are the one estimate in the ledger: Gemini's embedContent returns no usageMetadata,
so input tokens are estimated from the text (~4 chars/token) and the event carries
metadata.estimated = true. An estimate on a $0.15/M input-only model beats leaving catalog
embedding — which runs over every product an org has — off the bill entirely.
voice_agent lives in its own table because Gemini Live reports audio and text token counts separately; the three get_ai_consumption_* RPCs UNION ALL it in so the global page is genuinely global.
Why calls and meetings needed this¶
Before this change, Call Intelligence and Meetings only ever stored raw token counts (call_records.ai_tokens_*, call_token_usage). The dollar figure was invented in the browser against hard-coded Gemini 2.5 Flash rates. Two things were wrong with that:
- The vendor was assumed. An org that points Call Intelligence at OpenAI or Claude through
call_ai_vendor_configwas being billed at Flash rates on screen and Sonnet rates in reality — off by ~6x. - Transcription was billed as input.
transcribeFullAudiosummed input and output into one number, and the pipeline wrote the whole thing toai_tokens_input. A transcription is almost entirely output tokens, which every vendor charges several times more for, so a meeting's cost was understated several-fold.
Both are fixed: the platform now carries per-vendor sheets (OpenAI, Anthropic, Z.ai/GLM, self-hosted Ollama at $0) selected by the vendor that actually served the call, and transcription tokens are split and priced correctly end to end.
Data model¶
ai_usage_events
feature TEXT NOT NULL DEFAULT 'bot_flows'
call_record_id UUID REFERENCES call_records(id) ON DELETE CASCADE
audio_seconds NUMERIC(12,2) NOT NULL DEFAULT 0
node_type -- pipeline stage for calls/meetings
…
call_records
ai_cost_usd NUMERIC(12,6) NOT NULL DEFAULT 0
call_records.ai_cost_usd is cumulative, and holds one invariant:
call_records.ai_cost_usdalways equals the sum of that record's rows inai_usage_events.
That matters because a meeting is not one run: a long recording transcribes across several self-re-invocations, and a reprocess may keep the existing transcript and only re-run analysis. Each invocation adds its own stages onto whatever earlier ones already spent.
RPCs¶
Aggregation happens in Postgres, not the client — PostgREST caps a read at 1000 rows and a busy org bills far more AI calls than that in a month.
| RPC | Returns |
|---|---|
get_ai_consumption_by_feature(org, start, end) |
one row per feature: calls, tokens, audio seconds, cost |
get_ai_consumption_by_model(org, start, end, feature?) |
provider/model mix, optionally scoped to one feature |
get_ai_consumption_daily(org, start, end) |
per-day, per-feature cost for the trend chart |
All three are SECURITY DEFINER with SET search_path = public, pg_catalog, and raise 42501 unless the caller's user_auth_summary org matches (service role bypasses).
UI¶
| Surface | What it shows |
|---|---|
/billing/ai-usage |
KPI cards (spend, tokens, calls, audio + $/min), per-feature table with share bars, stacked daily trend, model mix |
| Call Intelligence dashboard | CallTokenUsageCard — this month's real spend, priced per call, with the top model |
| Call detail dialog | CallAiCostBreakdown — per-stage table (transcription / analysis / repair) |
| Meeting detail dialog | Same component under an AI cost tab — the transcription-vs-minutes split |
Records processed before this shipped have token counts but no ledger rows. Both surfaces say so explicitly rather than showing a confident wrong number: the dashboard card falls back to a labelled Flash-rate estimate, and the per-record breakdown says only the total is known.
Pricing accuracy¶
Rates in the platform are published list prices (Gemini verified 2026-08-05; other vendors 2026-09-10). They are estimates for attribution, not invoices — ConnectGain runs on the org's own provider keys, so the provider bills the org directly and the provider's invoice is authoritative. Costs are computed at write time and stored, so rate-table changes only affect new events; redeploy the edge functions after updating rates.
GLM's glm-4-flash is priced at $0 (Z.ai's free tier) and Ollama at $0 (self-hosted — the org already paid for the hardware). A period whose only usage is free must still render a 0% share rather than NaN%; summarizeFeatureRows guards that, with a test.
Deploying¶
- Apply
20260910150000_ai_usage_feature_dimension.sql - Redeploy every edge function that imports the platform, the platform, the platform, the platform, the platform or the platform — shared modules are bundled per function, so a function that is not redeployed keeps the old un-metered copy and stays invisible on the billing page
- Regenerate the platform so the new columns and RPCs are typed
No backfill is possible: providers do not expose per-record attribution retrospectively. The ledger starts at deploy time, and older recordings keep their token counts only.