feat(langchain): forward provider-reported cost to costDetails#830
Open
guillaume86 wants to merge 1 commit into
Open
feat(langchain): forward provider-reported cost to costDetails#830guillaume86 wants to merge 1 commit into
guillaume86 wants to merge 1 commit into
Conversation
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>
|
@guillaume86 is attempting to deploy a commit to the langfuse Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The LangChain
CallbackHandlerforwards tokenusageDetailsand the model name to Langfuse, but nevercostDetails. OpenAI-compatible providers (e.g. OpenRouter) report the monetary cost of a generation onresponse_metadata.usage.cost. That value is dropped, so generations are ingested with emptycost_details/nulltotalCostunless 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.handleLLMEndnow extractsresponse_metadata.usage.cost(via a newextractCostDetailshelper that mirrors the existingextractUsageMetadata/extractModelNameFromMetadata) and forwards it ascostDetails: { total }when it is a finite number. Provider-agnostic, and a no-op for providers that don't report a cost.Verification
pnpm lintpnpm typecheckpnpm test:integration(existing LangChain integration test still passes)pnpm format:checkpnpm 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.totalis 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, assertingcostDetailson the exported span). However, cost extraction — like the existingusageDetails/modelextraction — is gated onmessage instanceof AIMessage, and in the integration harness a synthetic message can't satisfy that check: the lockfile resolves two@langchain/coreversions (1.1.24and1.1.8, the latter via anopenai@6.22.0peer path), so the handler (packages/langchain→1.1.8) and the test (root →1.1.24) end up with differentAIMessageclasses. 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/corein the test setup.Release info
Bump level
Libraries affected
Changelog notes
usage.cost) to Langfuse ascostDetails.Greptile Summary
This PR adds a
extractCostDetailsprivate method toCallbackHandlerthat readsresponse_metadata.usage.costfrom the last LLM response and forwards it ascostDetails: { total: cost }to Langfuse, mirroring the existing pattern forextractUsageMetadataandextractModelNameFromMetadata.extractCostDetailshelper 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.totalkey used in{ total: cost }is consistent with howcostDetailsis populated elsewhere in the codebase (e.g.,packages/openai/src/traceMethod.ts), and the typeRecord<string, number>matches the expectedcostDetailsfield inCreateGenerationBody.output.llmOutputfor cost is provided (unlike thetokenUsagefallback inusageDetails), but this is intentional — the feature specifically targets theresponse_metadata.usage.costpath 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 costDetailsReviews (1): Last reviewed commit: "feat(langchain): forward provider-report..." | Re-trigger Greptile