Skip to content

Mirror LLM usage to PostHog AI observability (analytics view, not the ledger) #516

Description

@kcarnold

Context

feat/llm-usage-metering added per-user LLM cost metering: the proxy records a
content-free row (user, model, token counts, status, duration, ttft) into the
llm_usage table, and GET /api/usage_summary breaks spend down by user, costed
at read time from the hand-maintained rate table in backend/src/pricing.ts.

PostHog has an AI observability product
that overlaps: $ai_generation events carry provider/model/tokens/cost/latency, it
computes cost itself, and its dashboard answers "LLM costs by customer and model".
Privacy mode omits
$ai_input / $ai_output_choices, so no prompt or completion content is sent.

Decision: in addition, not instead

llm_usage stays the system of record. It must be, because:

  • Consent. PostHog is the channel our consent levels gate (opt-out by default,
    opt in at >= usage). Usage metering deliberately sits outside those levels —
    it's operational billing data we keep even at level none. Putting the ledger in
    PostHog would mean either capturing analytics for users who opted out, or having
    no cost data for them.
  • Durability. PostHog strips large $ai_ properties after 30 days and events
    age out under retention; a billing record needs to reconcile against an invoice
    months later.
  • Deletion. PostHog person deletion removes the events, so a departing user
    would take their spend history with them — exactly what the deleted tombstone
    in anonymizeUserUsage exists to prevent.

What a mirror would buy us

  • An independently-maintained price table. pricing.ts is hand-maintained and
    will silently rot the day we switch models (unlisted models report costUsd: null
    and appear in unpricedModels, so at least it fails loudly). PostHog's own cost
    calculation is a free cross-check.
  • Dashboards next to product analytics: which generation mode costs most per
    suggestion, whether TTFT correlates with drop-off, cost per feature. Our JSON
    summary endpoint will never be that.

Sketch

  • Emit $ai_generation from meter() in backend/src/openaiProxy.ts alongside
    recordUsage() — we already have every number. Note our proxy is a raw fetch
    passthrough with no OpenAI SDK, so none of PostHog's drop-in wrappers apply; the
    event is hand-rolled via posthog-node.
  • Only for users at consent >= usage (the proxy currently resolves the user id
    but not their consent level — small change to resolveUserId). PostHog's picture
    is therefore deliberately incomplete; that's fine, it's the analytics view.
  • Privacy mode on, and never send $ai_input / $ai_output_choices.
  • Add PostHog to the privacy policy's third-party list as a recipient of
    content-free token/cost telemetry (today it's listed for product analytics and
    error monitoring only).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions