Skip to content

fix: Capture reasoning in mistral#1863

Merged
Luca Forstner (lforst) merged 4 commits intomainfrom
lforst/mistral-thinking
Apr 24, 2026
Merged

fix: Capture reasoning in mistral#1863
Luca Forstner (lforst) merged 4 commits intomainfrom
lforst/mistral-thinking

Conversation

@lforst
Copy link
Copy Markdown
Member

Fixes #1857

@lforst Luca Forstner (lforst) changed the title fix: Capture streaming in mistral fix: Capture thinking in mistral Apr 21, 2026
@lforst Luca Forstner (lforst) changed the title fix: Capture thinking in mistral fix: Capture reasoning in mistral Apr 21, 2026
@lforst Luca Forstner (lforst) marked this pull request as ready for review April 21, 2026 15:35
left: MistralContentPart[] | undefined,
right: MistralContentPart[],
): MistralContentPart[] {
const merged = [...(left || []).map((part) => structuredClone(part))];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

structuredClone here alongside merged.push(structuredClone(part)); is pretty expensive. Can we make that better?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I find structuredClone here to be already relatively efficient and concise. Unless we lift that we want to be handling cloned objects and instead use the originals (more potential to screw up) I don't quite see an alternative that is much better. Did you have something in mind?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I guess that is fair, the objects are pretty small.

I chatted with opus and got something like so, but honestly it's more of a stylistic difference.

function mergeMistralTextSegmentsInPlace(
  left: MistralTextContentPart[],
  right: MistralTextContentPart[],
): MistralTextContentPart[] {
  for (const part of right) {
    const lastPart = left[left.length - 1];
    if (
      lastPart &&
      lastPart.type === "text" &&
      typeof lastPart.text === "string" &&
      typeof part.text === "string"
    ) {
      lastPart.text += part.text;
      continue;
    }

    left.push(part);
  }

  return left;
}

function mergeMistralContentParts(
  left: MistralContentPart[] | undefined,
  right: MistralContentPart[],
): MistralContentPart[] {
  const merged = left ?? [];

  for (const part of right) {
    const lastPart = merged[merged.length - 1];
    if (
      part.type === "text" &&
      lastPart?.type === "text" &&
      typeof lastPart.text === "string" &&
      typeof part.text === "string"
    ) {
      lastPart.text += part.text;
      continue;
    }

    if (
      part.type === "thinking" &&
      lastPart?.type === "thinking" &&
      Array.isArray(lastPart.thinking) &&
      Array.isArray(part.thinking)
    ) {
      mergeMistralTextSegmentsInPlace(lastPart.thinking, part.thinking);
      continue;
    }

    merged.push(part);
  }

  return merged;
}

: [];

if (thinking.length === 0) {
return undefined;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we preserve the thinking marker here? And just return an empty thinking array?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I get your thought but I would just be like "wtf" as a user on braintrust. It's sort of irrelevant to know that there was 0 thinking right? Unless maybe the user wants to use thinking but the model decided not to think. In any case, probably a big edge case - we can return an empty array!

left: MistralContentPart[] | undefined,
right: MistralContentPart[],
): MistralContentPart[] {
const merged = [...(left || []).map((part) => structuredClone(part))];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I guess that is fair, the objects are pretty small.

I chatted with opus and got something like so, but honestly it's more of a stylistic difference.

function mergeMistralTextSegmentsInPlace(
  left: MistralTextContentPart[],
  right: MistralTextContentPart[],
): MistralTextContentPart[] {
  for (const part of right) {
    const lastPart = left[left.length - 1];
    if (
      lastPart &&
      lastPart.type === "text" &&
      typeof lastPart.text === "string" &&
      typeof part.text === "string"
    ) {
      lastPart.text += part.text;
      continue;
    }

    left.push(part);
  }

  return left;
}

function mergeMistralContentParts(
  left: MistralContentPart[] | undefined,
  right: MistralContentPart[],
): MistralContentPart[] {
  const merged = left ?? [];

  for (const part of right) {
    const lastPart = merged[merged.length - 1];
    if (
      part.type === "text" &&
      lastPart?.type === "text" &&
      typeof lastPart.text === "string" &&
      typeof part.text === "string"
    ) {
      lastPart.text += part.text;
      continue;
    }

    if (
      part.type === "thinking" &&
      lastPart?.type === "thinking" &&
      Array.isArray(lastPart.thinking) &&
      Array.isArray(part.thinking)
    ) {
      mergeMistralTextSegmentsInPlace(lastPart.thinking, part.thinking);
      continue;
    }

    merged.push(part);
  }

  return merged;
}

@lforst Luca Forstner (lforst) merged commit fbd8d21 into main Apr 24, 2026
47 of 48 checks passed
@lforst Luca Forstner (lforst) deleted the lforst/mistral-thinking branch April 24, 2026 10:16
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.

[bot] Mistral streaming drops thinking content blocks from Magistral and Mistral Small 4 reasoning models

3 participants