Skip to content

fix(langchain): read usage_metadata via duck-typing to support multiple @langchain/core copies#843

Open
seb7152 wants to merge 1 commit into
langfuse:mainfrom
seb7152:fix/langchain-usage-metadata-duck-typing
Open

fix(langchain): read usage_metadata via duck-typing to support multiple @langchain/core copies#843
seb7152 wants to merge 1 commit into
langfuse:mainfrom
seb7152:fix/langchain-usage-metadata-duck-typing

Conversation

@seb7152

@seb7152 seb7152 commented Jun 16, 2026

Copy link
Copy Markdown

Problem

CallbackHandler extracts token usage and model name with:

generation["message"] instanceof AIMessage ||
generation["message"] instanceof AIMessageChunk

When the host application and @langfuse/langchain resolve different copies of @langchain/core (pnpm/bun workspaces, nested deps, n8n community nodes, etc.), the message is an instance of the other copy's class. Cross-realm instanceof returns false, so the handler falls through to undefined and silently drops usageDetails (output, output_reasoning, input_cache_read) and the model name — even though the model returned them.

Fixes #13075.

Fix

Use duck-typing instead of class-identity checks — read message.usage_metadata and message.response_metadata.model_name directly. This is realm-independent and matches the workaround recommended in the issue. AIMessage / AIMessageChunk are no longer referenced, so they're removed from the import.

Verification

Reproduced with @langchain/core 1.x where the message originates from a different @langchain/core instance than the one bundled with the langchain integration (OpenAI Chat Completions and Anthropic, via n8n's langfuse-langchain):

  • Before: extractUsageMetadata returns undefined for a message that is not an instance of the bundled AIMessage → output/reasoning tokens missing in Langfuse.
  • After: the same message's usage_metadata is read correctly → output (and reasoning/cached) tokens appear.

Scope intentionally limited to the usage/model extraction from #13075. The instanceof BaseMessage checks on chain inputs have the same theoretical fragility but are a separate concern and out of scope here.

🤖 Generated with Claude Code

Greptile Summary

Replaces instanceof AIMessage / AIMessageChunk checks in the LangChain callback handler with duck-typed property access (?.["message"]?.usage_metadata) so that usage metadata and model name are correctly extracted even when the host application and @langfuse/langchain resolve different copies of @langchain/core.

  • extractUsageMetadata now reads generation?.["message"]?.usage_metadata directly, dropping the now-unnecessary AIMessage/AIMessageChunk imports and their cross-realm-fragile identity checks.
  • extractModelNameFromMetadata receives the same treatment for response_metadata.model_name, with both changes guarded by optional chaining so non-chat generations safely return undefined.

Confidence Score: 5/5

Safe to merge — the change is a narrow, well-scoped fix that removes a silent data-loss path without altering any other behavior.

Both helpers now read the relevant fields via optional-chaining duck-typed access instead of instanceof class-identity checks. Non-chat generations fall through to undefined exactly as before, the downstream ?? fallback chain in handleLLMEnd is untouched, and the removed imports were the only consumers of AIMessage/AIMessageChunk in the file. No new code paths are introduced.

No files require special attention.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant LLM as LLM / Chain
    participant CH as CallbackHandler
    participant EU as extractUsageMetadata()
    participant EM as extractModelNameFromMetadata()

    LLM->>CH: handleLLMEnd(output, runId)
    CH->>CH: "lastResponse = output.generations[last][last]"
    CH->>EU: extractUsageMetadata(lastResponse)
    Note over EU: Before: instanceof AIMessage/AIMessageChunk<br/>(fails across @langchain/core copies)
    Note over EU: After: (generation as any)?.["message"]?.usage_metadata<br/>(duck-typing, realm-independent)
    EU-->>CH: "UsageMetadata | undefined"
    CH->>EM: extractModelNameFromMetadata(lastResponse)
    Note over EM: Before: instanceof check + response_metadata.model_name<br/>After: generation?.["message"]?.response_metadata?.model_name
    EM-->>CH: "string | undefined"
    CH->>CH: Build usageDetails, update span
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant LLM as LLM / Chain
    participant CH as CallbackHandler
    participant EU as extractUsageMetadata()
    participant EM as extractModelNameFromMetadata()

    LLM->>CH: handleLLMEnd(output, runId)
    CH->>CH: "lastResponse = output.generations[last][last]"
    CH->>EU: extractUsageMetadata(lastResponse)
    Note over EU: Before: instanceof AIMessage/AIMessageChunk<br/>(fails across @langchain/core copies)
    Note over EU: After: (generation as any)?.["message"]?.usage_metadata<br/>(duck-typing, realm-independent)
    EU-->>CH: "UsageMetadata | undefined"
    CH->>EM: extractModelNameFromMetadata(lastResponse)
    Note over EM: Before: instanceof check + response_metadata.model_name<br/>After: generation?.["message"]?.response_metadata?.model_name
    EM-->>CH: "string | undefined"
    CH->>CH: Build usageDetails, update span
Loading

Reviews (1): Last reviewed commit: "fix(langchain): read usage_metadata via ..." | Re-trigger Greptile

…le @langchain/core copies

The CallbackHandler extracted token usage and model name using
`generation.message instanceof AIMessage / AIMessageChunk`. When the host
application and @langfuse/langchain resolve different copies of
@langchain/core (pnpm/bun workspaces, nested deps, n8n community nodes,
etc.), the message is an instance of the *other* copy's class, so the
`instanceof` checks return false. As a result usageDetails (output,
output_reasoning, input_cache_read) and the model name are silently
dropped, even though the model returns them.

Use duck-typing instead: read `message.usage_metadata` and
`message.response_metadata.model_name` directly. This is realm-independent
and matches the workaround recommended in the issue.

Fixes #13075

Co-Authored-By: Claude Opus 4.8 <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 16, 2026

Copy link
Copy Markdown

Someone 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 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@seb7152

seb7152 commented Jun 21, 2026

Copy link
Copy Markdown
Author

Recheck

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.

2 participants