Skip to content

ChatOpenAI drops streamed tool calls when the server sends role: "" #11468

Description

@Sparky579

Checked other resources

  • 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.

Example Code

import { ChatOpenAI } from "@langchain/openai";

const chunk = (delta, finish = null) =>
  `data: ${JSON.stringify({
    id: "c",
    object: "chat.completion.chunk",
    created: 1,
    model: "m",
    choices: [{ index: 0, delta, finish_reason: finish }],
  })}\n\n`;

// A server that sends role: "" instead of "assistant".
const sse =
  chunk({
    role: "",
    tool_calls: [
      {
        index: 0,
        id: "call_1",
        type: "function",
        function: { name: "get_weather", arguments: '{"city":"BJ"}' },
      },
    ],
  }) +
  chunk({}, "tool_calls") +
  "data: [DONE]\n\n";

const model = new ChatOpenAI({
  model: "m",
  apiKey: "x",
  configuration: {
    baseURL: "https://example.invalid/v1",
    fetch: async () =>
      new Response(sse, {
        status: 200,
        headers: { "content-type": "text/event-stream" },
      }),
  },
});

for await (const c of await model.stream("hi")) {
  console.log(c.tool_call_chunks);
}

Prints undefined for every chunk. Changing role: "" to role: "assistant" in the same script prints the expected tool_call_chunk.

Error Message and Stack Trace (if applicable)

No error is raised. The tool call is dropped silently.

Description

ChatOpenAI loses streamed tool calls when a server sends role: "" instead of "assistant". convertCompletionsDeltaToBaseMessageChunk resolves the role with delta.role ?? defaultRole, and ?? only falls back on null/undefined, so an empty string reaches the final branch and the delta is converted to a ChatMessageChunk rather than an AIMessageChunk. Any tool_calls on that delta are discarded and tool_call_chunks is undefined. Plain text still streams normally, which makes this easy to miss: the response looks healthy and only tool calls disappear.

Empty delta.role is something OpenAI-compatible servers do in practice — anomalyco/opencode#28427 reports the same responses from the consumer side. #11157 fixes the closely related case where role is omitted entirely, but keeps ??, so with that PR applied the empty-string variant still fails. Coalescing with || instead covers both, and explicit "tool" / "system" / "user" roles keep working since those are truthy.

System Info

@langchain/openai 1.5.8
@langchain/core 1.2.9
node v20.19.5
linux x64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions