Skip to content

Langfuse Observability Oktivo ​

Track LLM tokens, costs, and call metadata across all 3 services filterable by organization, customer, campaign, and individual call.


Table of Contents ​

  1. Environment Setup
  2. Architecture Overview
  3. Files Changed for Langfuse
  4. What Data Goes to Langfuse
  5. What Contributes to Tokens & Cost
  6. Dashboard Setup (Step by Step)
  7. Drill-Down: Org → Customer → Campaign → Call
  8. Custom Model Pricing (Fixes $0.00)
  9. Filter Cheat Sheet
  10. Adding Observability to New Code
  11. Troubleshooting

Environment Setup ​

Each service needs these env vars (same Langfuse project keys per environment):

env
# Python services (Agent, Analysis)
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com

# Node.js service (client_backend)
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_BASEURL=https://cloud.langfuse.com

Use separate Langfuse projects per environment (dev/stage/prod) so data never mixes.

To get keys: Langfuse dashboard → Project Settings → API Keys.


Architecture Overview ​

┌─────────────────────────────────────────────────────────────┐
│  client_backend (Node.js)                                   │
│                                                             │
│  calling.service.ts ──► trackCallDispatch()                 │
│  openai.service.ts  ──► trackGeneration() [hard_words]      │
│  webhook.controller ──► trackCallCompletion()               │
│                  │                                          │
│          langfuse.service.ts (Langfuse JS SDK)              │
│          + name cache (org/customer/campaign names)         │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
              Langfuse Cloud
                       ▲
                       │
┌──────────────────────┼──────────────────────────────────────┐
│  Agent (Python)      │                                      │
│                      │                                      │
│  LiveKit Agents ──► OTel TracerProvider ──► Langfuse SDK    │
│  (auto STT/LLM/TTS spans)                                  │
│                                                             │
│  post_call.py ──► call_usage_summary trace                  │
│                   (voice_call_llm generation with tokens)   │
│  telemetry.py ──► Langfuse init + OTel setup                │
└──────────────────────┼──────────────────────────────────────┘
                       │
┌──────────────────────┼──────────────────────────────────────┐
│  Analysis (Python)   │                                      │
│                      │                                      │
│  @observe decorators on orchestrator + openai_service       │
│  openai_service.py ──► @observe(as_type="generation")       │
│                        (auto-captures OpenAI tokens/cost)   │
│  config.py ──► Langfuse client init                         │
└─────────────────────────────────────────────────────────────┘

Files Changed for Langfuse ​

Below is every file that was added or modified for Langfuse integration.

Agent Service (Python) ​

FileWhat was changed
Agent/src/services/telemetry.pyNew file. Initializes Langfuse SDK + OTel TracerProvider so LiveKit auto-emitted STT/LLM/TTS spans flow to Langfuse. Provides get_langfuse_client() and flush_langfuse() helpers.
Agent/agent.pyCalls setup_langfuse_telemetry() at worker startup. Calls flush_langfuse() on shutdown. Passes model_info dict (stt_model, llm_model, tts_model) to post_call_event. Collects usage metrics via UsageCollector.
Agent/src/services/post_call.pyRewritten. Sends a call_usage_summary trace to Langfuse via the Python SDK with 3 generation observations: voice_call_llm (input/output tokens), voice_call_tts (characters), voice_call_stt (milliseconds). Also passes room_name + human-readable names to Analysis service.
Agent/src/schemas/metadata_schema.pyAdded optional fields: org_name, customer_name, customer_email, campaign_name for Langfuse name resolution.
Agent/src/services/metadata_resolver.pyAdded _normalize_dispatch_fields() to map orgUID→organization_id, customerUID→customer_id from dispatch metadata.

Analysis Service (Python) ​

FileWhat was changed
Analysis/config.pyAdded _init_langfuse() to initialize Langfuse client. Provides get_langfuse() helper.
Analysis/app.pyAdded Langfuse flush on FastAPI shutdown via lifespan handler.
Analysis/src/services/openai_service.pyAdded @observe(as_type="generation") decorator on structured_json_call(). This auto-captures OpenAI token usage and cost.
Analysis/src/orchestration/analysis_orchestrator.pyAdded @observe(name="conversation_analysis") decorator. Uses propagate_attributes() to tag traces with user_id (org UID), session_id (room_name), tags (org:, customer:, campaign:, analysis), and metadata (all names). Added _resolve_names() DB fallback. Accepts room_name + name fields from Agent.
Analysis/src/routes/analysis_routes.pyPasses room_name, org_name, customer_name, customer_email, campaign_name through to orchestrator.
Analysis/src/schemas/analysis_schema.pyAdded fields: room_name, org_name, customer_name, customer_email, campaign_name to AnalysisRequest.

client_backend Service (Node.js) ​

FileWhat was changed
client_backend/src/services/langfuse.service.tsNew file. Langfuse singleton client + 5-min in-memory name cache. Exports: getLangfuse(), resolveOrgName(), resolveCustomerInfo(), resolveCampaignName(), trackCallDispatch(), trackGeneration(), trackCallCompletion(), flushLangfuse().
client_backend/src/services/openai.service.tsExtracts token usage from OpenAI response (input_tokens, output_tokens, total_tokens) and passes to trackGeneration() for hard_words_extraction.
client_backend/src/services/calling.service.tsResolves org/customer/campaign names before dispatch. Passes names in DispatchMetadata. Calls trackCallDispatch() after each call dispatch.
client_backend/src/services/livekit.service.tsAdded org_name, customer_name, customer_email, campaign_name to DispatchMetadata interface.
client_backend/src/controllers/webhook.controller.tsCalls trackCallCompletion() on participant_left with billing duration, call status, disconnect reason.
client_backend/src/controllers/agent.controller.tsTest call dispatch resolves org/customer names via resolveOrgName() and resolveCustomerInfo() before dispatch.

What Data Goes to Langfuse ​

6 things are sent to Langfuse: ​

#Trace NameSourceWhat it containsWhere it shows in Langfuse
1call_dispatchclient_backend calling.service.tsRoom name, contact name, phone, campaign, agent, org/customer namesTracing tab → metadata. Tags: dispatch, org:, customer:, campaign:
2call_completionclient_backend webhook.controller.tsBilling duration (seconds), call status, disconnect reason, org/customer/campaign namesTracing tab → metadata. Tags: call_completion, org:, customer:, campaign:
3hard_words_extractionclient_backend openai.service.tsOpenAI input/output tokens, model, prompt, extracted wordsTracing tab → has a generation observation with token counts
4LiveKit auto-spansAgent telemetry.py (OTel)Individual STT/LLM/TTS spans per turn latency, model, tokens (LLM only)Tracing tab → auto-created traces with observations
5call_usage_summaryAgent post_call.pyAggregated per-call: LLM tokens, TTS characters, STT duration, all org/customer/campaign metadataTracing tab → has voice_call_llm generation with native token counts
6conversation_analysisAnalysis analysis_orchestrator.pyOpenAI tokens/cost from analysis, org/customer/campaign metadataTracing tab → has structured_json_call generation with auto-captured tokens

Every trace carries these attributes: ​

AttributeLangfuse fieldValue
Organization UIDUser Idorg UID (e.g. org_zWFf1nZLL1N8CFut)
Organization Namemetadata.orgNameHuman-readable name
Customer UIDTag customer:{uid} + metadata.customer_idCustomer UID
Customer Namemetadata.customerNameHuman-readable name
Customer Emailmetadata.customerEmailEmail address
Campaign UIDTag campaign:{uid} + metadata.campaign_idCampaign UID
Campaign Namemetadata.campaignNameHuman-readable name
Call SessionSession IdLiveKit room name (e.g. room-5c1d78bd)

What Contributes to Tokens & Cost ​

Only generation type observations with token counts feed into Langfuse's Total Tokens and Total Cost metrics.

GenerationTraceTokens?Cost?
voice_call_llmcall_usage_summaryYes input + output tokensYes (after model pricing configured)
voice_call_ttscall_usage_summaryNo uses CHARACTERS unitOnly if character-based pricing configured
voice_call_sttcall_usage_summaryNo uses MILLISECONDS unitOnly if time-based pricing configured
structured_json_callconversation_analysisYes auto-captured from OpenAIYes OpenAI pricing is built-in
hard_words_extractionhard_words_extractionYes input + output tokensYes OpenAI pricing is built-in
LiveKit LLM spansAuto-createdYes gen_ai.usage.* OTel attributesYes (after model pricing configured)

For your core requirement (total input tokens, output tokens, cost per call/customer/org):

  • LLM tokens come from voice_call_llm + structured_json_call + hard_words_extraction
  • Filter by User Id for per-org, by customer: tag for per-customer, by Session Id for per-call

The other traces (call_dispatch, call_completion) are metadata-only they show call lifecycle info but contribute zero to token/cost numbers.


Dashboard Setup (Step by Step) ​

Langfuse version: v3.160+ (Hobby plan or above)

Step 1: Create the Dashboard ​

  1. Go to https://cloud.langfuse.com → select your project
  2. Left sidebar → Dashboards
  3. Click "New Dashboard" → name it Oktivo Operations

Step 2: Add Widgets ​

Click "+ Add Chart" for each widget. Here are the exact settings:


Widget 1: Cost by Organization

SettingValue
ViewTraces
MetricTotal Cost
Breakdown DimensionUser Id
Chart TypeHorizontal Bar Chart
NameCost by Organization

Each bar = one org's total LLM cost. Shows $0.00 until custom model pricing is configured.


Widget 2: Cost by Customer

SettingValue
ViewTraces
MetricTotal Cost
Breakdown DimensionTags
Chart TypeHorizontal Bar Chart
NameCost by Customer

Shows cost grouped by all tags. Look for customer: prefixed entries for per-customer cost.


Widget 3: Tokens by Organization

SettingValue
ViewTraces
MetricTotal Tokens
Breakdown DimensionUser Id
Chart TypeHorizontal Bar Chart
NameTokens by Organization

Widget 4: Cost by Model

SettingValue
ViewObservations
MetricTotal Cost
Breakdown DimensionModel
Chart TypePie Chart
NameCost by Model

Use Observations view (not Traces) because model info lives on individual observations.


Widget 5: Token Usage Over Time

SettingValue
ViewTraces
MetricTotal Tokens
Breakdown DimensionNone
Chart TypeLine Chart (Time Series)
NameToken Usage Trend

Widget 6: Total Calls by Organization

SettingValue
ViewTraces
MetricCount
Breakdown DimensionUser Id
Chart TypeHorizontal Bar Chart
NameCalls by Organization

Widget 7: Daily Call Volume

SettingValue
ViewTraces
MetricUnique Session Ids
Breakdown DimensionNone
Chart TypeLine Chart (Time Series)
NameDaily Call Volume

Each unique session = one call (room name).


Widget 8: Latency by Model

SettingValue
ViewObservations
MetricLatency
Breakdown DimensionModel
Chart TypeHorizontal Bar Chart
NameLatency by Model

Shows which provider (Deepgram, Gemini, Cartesia) is slowest.


Widget 9: Trace Types Breakdown

SettingValue
ViewTraces
MetricCount
Breakdown DimensionName
Chart TypePie Chart
NameTrace Types

Shows distribution: call_dispatch, call_completion, conversation_analysis, call_usage_summary, hard_words_extraction.


Widget 10: Active Organizations

SettingValue
ViewTraces
MetricUnique User Ids
Breakdown DimensionNone
Chart TypeBig Number (Total Value)
NameActive Organizations

Widget 11: Activity by Tag

SettingValue
ViewTraces
MetricCount
Breakdown DimensionTags
Chart TypeHorizontal Bar Chart
NameActivity by Tag

Shows counts for all tags: org:, customer:, campaign:, dispatch, analysis, call_completion, usage_summary.


Drill-Down: Org → Customer → Campaign → Call ​

See all traces for one organization ​

  1. Left sidebar → Tracing
  2. Add filter: User ID = org UID
  3. All traces for that org appear with cost/token columns

See one customer within an org ​

  1. Same Tracing tab with org filter
  2. Add filter: Tags → contains customer:{customerUID}
  3. Or: filter by Metadata → key: customerName, value: the name

See one campaign ​

  1. Add filter: Tags → contains campaign:{campaignUID}
  2. Or: filter by Metadata → key: campaignName, value: the name

See one call (full lifecycle) ​

  1. Left sidebar → Sessions
  2. Click on a session (room name like room-5c1d78bd)
  3. Shows all traces for that call grouped together:
    • call_dispatch call initiated
    • LiveKit STT/LLM/TTS spans per-turn details
    • call_usage_summary aggregated LLM tokens
    • conversation_analysis analysis OpenAI tokens
    • call_completion billing duration, status

Agent + Analysis traces share the same session_id (room name), so they appear together.


Custom Model Pricing (Fixes $0.00) ​

Langfuse auto-prices OpenAI models. For all other providers, you must add pricing manually.

  1. Left sidebar → Settings (bottom)
  2. Go to "Models" tab
  3. Click "Add Model" for each:
Model name (must match exactly)ProviderPricing source
gemini-2.5-flashGooglehttps://ai.google.dev/pricing
nova-3Deepgram (STT)https://deepgram.com/pricing
sonic-turboCartesia (TTS)https://cartesia.ai/pricing
saaras:v3Sarvam (STT)https://sarvam.ai/pricing
ElevenLabs modelElevenLabs (TTS)https://elevenlabs.io/pricing
FalconMurf (TTS)https://murf.ai/pricing

Tip: Before adding pricing, go to Tracing → click any observation → check the Usage section to see what unit Langfuse received (tokens, characters, milliseconds). Match your pricing unit to that.

Once configured, all cost widgets and columns show real dollar amounts.


Filter Cheat Sheet ​

I want to see...Filter
Everything for org XUser ID = org UID
Customer Y within org XUser ID = org UID + Tags contains customer:{uid}
Customer Y by nameMetadata → key: customerName, value: name
Campaign ZTags contains campaign:{campaignUID}
Campaign Z by nameMetadata → key: campaignName, value: name
Single callSession ID = room name
Only analysis tracesTags contains analysis
Only dispatchesTags contains dispatch
Only completionsTags contains call_completion
Only usage summariesTags contains usage_summary
Full call lifecycleSession ID = room name (Agent + Analysis traces share this)

Adding Observability to New Code ​

Python New LLM call ​

python
from langfuse import observe

class MyService:
    @observe(as_type="generation")
    async def my_llm_call(self, prompt: str):
        response = await openai_client.responses.create(
            model="gpt-4o-mini",
            input=[{"role": "user", "content": prompt}],
        )
        return response.output[0].content[0].text

Python Tag with org metadata ​

python
from langfuse import observe, propagate_attributes

@observe(name="my_operation")
async def my_function(self, org_id: str):
    with propagate_attributes(
        user_id=org_id,
        tags=[f"org:{org_id}"],
        metadata={"organization_id": org_id},
    ):
        # child spans inherit these attributes
        pass

Node.js New tracked event ​

typescript
import { getLangfuse } from "./langfuse.service.js";

const lf = getLangfuse();
if (lf) {
  const trace = lf.trace({
    name: "my_operation",
    userId: orgUID,
    tags: [`org:${orgUID}`],
  });
  trace.generation({
    name: "my_llm_call",
    model: "gpt-4o-mini",
    input: prompt,
    output: result,
    usage: { input: inputTokens, output: outputTokens },
  });
  await lf.flushAsync();
}

Troubleshooting ​

ProblemFix
No traces appearingVerify LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set correctly
All costs $0.00Configure custom model pricing: Settings → Models (see section above)
Agent traces missing org metadataCheck metadata_resolver.py maps orgUID → organization_id
Analysis traces have no token countsEnsure @observe(as_type="generation") is on the OpenAI call
Analysis traces not linked to callEnsure room_name is passed from Agent to Analysis
Traces in wrong projectWrong API keys each env needs its own project keys
Data not flushingCheck flush_langfuse() / lf.flushAsync() in shutdown handlers

Last updated: