Skip to content

feat(langchain): forward provider-reported cost to costDetails#830

Open
guillaume86 wants to merge 1 commit into
langfuse:mainfrom
guillaume86:feat/langchain-cost-details
Open

feat(langchain): forward provider-reported cost to costDetails#830
guillaume86 wants to merge 1 commit into
langfuse:mainfrom
guillaume86:feat/langchain-cost-details

Conversation

@guillaume86

@guillaume86 guillaume86 commented Jun 12, 2026

Copy link
Copy Markdown

Problem

The LangChain CallbackHandler forwards token usageDetails and the model name to Langfuse, but never costDetails. OpenAI-compatible providers (e.g. OpenRouter) report the monetary cost of a generation on response_metadata.usage.cost. That value is dropped, so generations are ingested with empty cost_details / null totalCost unless Langfuse can self-compute cost from its model price table — which fails for any model not in that table (most OpenRouter slugs included).

Closes #828

Changes

CallbackHandler.handleLLMEnd now extracts response_metadata.usage.cost (via a new extractCostDetails helper that mirrors the existing extractUsageMetadata / extractModelNameFromMetadata) and forwards it as costDetails: { total } when it is a finite number. Provider-agnostic, and a no-op for providers that don't report a cost.

+      const costDetails = this.extractCostDetails(lastResponse);
+
       const extractedOutput = ...;

       this.handleOtelSpanEnd({
         runId,
         type: "generation",
         attributes: {
           model: modelName,
           output: extractedOutput,
           completionStartTime: ...,
           usageDetails: usageDetails,
+          ...(costDetails !== undefined ? { costDetails } : {}),
         },
       });

Verification

  • pnpm lint
  • pnpm typecheck
  • pnpm test:integration (existing LangChain integration test still passes)
  • pnpm format:check
  • pnpm test:e2e (not run locally — requires a live Langfuse server)

Manually verified end-to-end against a self-hosted Langfuse + OpenRouter: with this change the generation's costDetails.total is populated with OpenRouter's reported cost (previously empty).

A note on testing

I wanted to add an automated test (a fake chat model reporting response_metadata.usage.cost, asserting costDetails on the exported span). However, cost extraction — like the existing usageDetails / model extraction — is gated on message instanceof AIMessage, and in the integration harness a synthetic message can't satisfy that check: the lockfile resolves two @langchain/core versions (1.1.24 and 1.1.8, the latter via an openai@6.22.0 peer path), so the handler (packages/langchain1.1.8) and the test (root → 1.1.24) end up with different AIMessage classes. This is also why there is currently no integration coverage for generation message extraction (only tool runs).

Happy to add a test in whatever form you prefer — e.g. an e2e test against a cost-reporting provider, or guidance on deduping @langchain/core in the test setup.

Release info

Bump level

  • Minor

Libraries affected

  • @langfuse/langchain

Changelog notes

  • Forward provider-reported generation cost (e.g. OpenRouter's usage.cost) to Langfuse as costDetails.

Greptile Summary

This PR adds a extractCostDetails private method to CallbackHandler that reads response_metadata.usage.cost from the last LLM response and forwards it as costDetails: { total: cost } to Langfuse, mirroring the existing pattern for extractUsageMetadata and extractModelNameFromMetadata.

  • The new extractCostDetails helper follows the same defensive guard pattern (instanceof AIMessage || instanceof AIMessageChunk, try-catch, finite-number validation) as the sibling extraction methods, making it a clean, minimal addition.
  • The total key used in { total: cost } is consistent with how costDetails is populated elsewhere in the codebase (e.g., packages/openai/src/traceMethod.ts), and the type Record<string, number> matches the expected costDetails field in CreateGenerationBody.
  • No fallback to output.llmOutput for cost is provided (unlike the tokenUsage fallback in usageDetails), but this is intentional — the feature specifically targets the response_metadata.usage.cost path used by OpenRouter and similar OpenAI-compatible providers.

Confidence Score: 4/5

Safe to merge — the change is a focused, additive extraction of a new metadata field with defensive guards throughout and no effect on existing paths.

The extractCostDetails method mirrors the existing extraction helpers precisely: same instanceof guard, same try-catch, same return type. The { total: cost } key is consistent with how costDetails is used in the OpenAI wrapper. The finite-number check correctly rejects NaN/Infinity. No pre-existing behaviour is altered; the spread is a no-op when the provider does not report cost.

No files require special attention.

Sequence Diagram

sequenceDiagram
    participant LC as LangChain
    participant CH as CallbackHandler
    participant ED as extractCostDetails
    participant OS as handleOtelSpanEnd

    LC->>CH: handleLLMEnd(output, runId)
    CH->>CH: "lastResponse = output.generations[last][last]"
    CH->>ED: extractCostDetails(lastResponse)
    ED->>ED: check "message" in generation
    ED->>ED: instanceof AIMessage / AIMessageChunk?
    alt is AIMessage/AIMessageChunk
        ED->>ED: read response_metadata.usage.cost
        alt cost is finite number
            ED-->>CH: "{ total: cost }"
        else cost missing / non-finite
            ED-->>CH: undefined
        end
    else not an AI message
        ED-->>CH: undefined
    end
    CH->>OS: "handleOtelSpanEnd({ ..., costDetails })"
    OS-->>LC: span recorded with costDetails
Loading

Reviews (1): Last reviewed commit: "feat(langchain): forward provider-report..." | Re-trigger Greptile

The CallbackHandler forwarded token usageDetails and the model name to
Langfuse but never costDetails. OpenAI-compatible providers (e.g.
OpenRouter) report the monetary cost of a generation on
`response_metadata.usage.cost`, which was dropped — so generations were
ingested with empty cost_details / null totalCost unless Langfuse could
self-compute cost from its model price table (which fails for models not
in that table, e.g. most OpenRouter slugs).

`handleLLMEnd` now reads `response_metadata.usage.cost` and forwards it as
`costDetails: { total }` when present. Provider-agnostic and a no-op for
providers that don't report cost.

Closes langfuse#828

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@guillaume86 is attempting to deploy a commit to the langfuse Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented Jun 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@langfuse/langchain CallbackHandler doesn't forward provider-reported cost (e.g. OpenRouter usage.cost) to costDetails

2 participants