Skip to content

_formatForTracing only converts the first base64 content block per message #11291

Description

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a very descriptive title to this issue.
  • I searched the LangChain.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

Example Code

import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import type { Serialized } from "@langchain/core/load/serializable";
import { HumanMessage } from "@langchain/core/messages";
import type { BaseMessage } from "@langchain/core/messages";
import { FakeListChatModel } from "@langchain/core/utils/testing";

class CaptureInputHandler extends BaseCallbackHandler {
  name = "CaptureInputHandler";
  messages: BaseMessage[] | undefined;

  async handleChatModelStart(
    _llm: Serialized,
    messages: BaseMessage[][],
  ): Promise<void> {
    this.messages = messages[0];
  }
}

// Shape still shown in the JS multimodal docs:
// https://docs.langchain.com/oss/javascript/langchain/messages#multimodal
const message = new HumanMessage({
  content: [
    { type: "text", text: "two PDFs" },
    {
      type: "file",
      source_type: "base64",
      mime_type: "application/pdf",
      data: "JVBERi0xLjQK",
    },
    {
      type: "file",
      source_type: "base64",
      mime_type: "application/pdf",
      data: "JVBERi0xLjUK",
    },
  ],
});

const handler = new CaptureInputHandler();
await new FakeListChatModel({ responses: ["ok"] }).invoke([message], {
  callbacks: [handler],
});

console.log(JSON.stringify(handler.messages?.[0]?.content, null, 2));

Error Message and Stack Trace (if applicable)

No response

Description

  • I'm trying to trace chat model inputs that contain several base64 file attachments in one HumanMessage using the data content block shape shown in the TypeScript > Messages > Multimodal docs (type + source_type + data + mime_type). Observability tools (e.g., Langfuse) rely on _formatForTracing rewriting those blocks into OpenAI image_url data URIs so media can be detected and offloaded.
  • I expect every matching base64/URL block in the message to be converted the same way before handleChatModelStart fires (tracing copy only — the provider still receives the original message).
  • Instead, only the first matching block is converted. Later blocks are left as authored file/image content, so their base64 stays inline in the trace.

Actual output

[
  {
    "type": "text",
    "text": "two PDFs"
  },
  {
    "type": "image_url",
    "image_url": {
      "url": "data:application/pdf;base64,JVBERi0xLjQK"
    }
  },
  {
    "type": "file",
    "source_type": "base64",
    "mime_type": "application/pdf",
    "data": "JVBERi0xLjUK"
  }
]

The second attachment is never rewritten, so its base64 stays inline in the trace.

Expected output

[
  {
    "type": "text",
    "text": "two PDFs"
  },
  {
    "type": "image_url",
    "image_url": {
      "url": "data:application/pdf;base64,JVBERi0xLjQK"
    }
  },
  {
    "type": "image_url",
    "image_url": {
      "url": "data:application/pdf;base64,JVBERi0xLjUK"
    }
  }
]

Root cause

In _formatForTracing (https://github.com/langchain-ai/langchainjs/blob/main/libs/langchain-core/src/language_models/chat_models.ts#L171), the clone guard if (messageToTrace === message) also gates the conversion. After the first rewrite, messageToTrace !== message, so subsequent blocks are skipped.

Docs / shape note

The conversion path only understands the deprecated isBase64ContentBlock / isURLContentBlock helpers (source_type). Standard multimodal blocks are not converted at all:

Shape Documented in Converted?
{ type, source_type, data, mime_type } JS multimodal docs Yes — but only the first block
{ type, base64, mime_type } Python multimodal docs No
{ type, data, mimeType } JS ContentBlock.Multimodal.File No

So today the only way to get attachments traced as data URIs is the deprecated shape — and even then only one attachment per message is rewritten.

Proposed fix

Clone once, then convert every matching block. We’ve verified this locally with a pnpm patch on @langchain/core@1.2.3.

System Info

@langchain/core: 1.2.3 (also present on main as of this report)
langchain: 1.5.3
Platform: macOS
Node: 24.18.0
pnpm: 11.15.1

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions