fix: send assistant content as string in openai-completions provider (#2008)

The OpenAI Chat Completions API standard format for assistant message
content is a plain string. Sending it as an array of
{type:"text", text:"..."} objects causes some models (notably DeepSeek
V3.2 via NVIDIA NIM) to mirror the content-block structure literally
in their output. This produces recursive nesting where each turn wraps
the previous content blocks deeper:

  [{'type':'text','text':'[{\'type\':\'text\',\'text\':...}]'}]

The fix unifies the assistant content serialization to always use a
joined string — the same approach already used for the github-copilot
provider — for all openai-completions backends.

Affected models observed: deepseek-ai/deepseek-v3.2 (nvidia provider).
Models like GLM-5, GPT-4, Claude were unaffected as they tolerate
array content, but sending a standard string is safer for all.

Co-authored-by: geraldoaax <geraldoaax@users.noreply.github.com>
This commit is contained in:
Geraldo Dutra Neto
2026-03-10 12:04:11 -03:00
committed by GitHub
parent 15e0957b04
commit 23109b113d

View File

@@ -559,15 +559,12 @@ export function convertMessages(
// Filter out empty text blocks to avoid API validation errors
const nonEmptyTextBlocks = textBlocks.filter((b) => b.text && b.text.trim().length > 0);
if (nonEmptyTextBlocks.length > 0) {
// GitHub Copilot requires assistant content as a string, not an array.
// Sending as array causes Claude models to re-answer all previous prompts.
if (model.provider === "github-copilot") {
assistantMsg.content = nonEmptyTextBlocks.map((b) => sanitizeSurrogates(b.text)).join("");
} else {
assistantMsg.content = nonEmptyTextBlocks.map((b) => {
return { type: "text", text: sanitizeSurrogates(b.text) };
});
}
// Always send assistant content as a plain string (OpenAI Chat Completions
// API standard format). Sending as an array of {type:"text", text:"..."}
// objects is non-standard and causes some models (e.g. DeepSeek V3.2 via
// NVIDIA NIM) to mirror the content-block structure literally in their
// output, producing recursive nesting like [{'type':'text','text':'[{...}]'}].
assistantMsg.content = nonEmptyTextBlocks.map((b) => sanitizeSurrogates(b.text)).join("");
}
// Handle thinking blocks