Skip to content

🪢 fix: Flatten Text-Only Assistant Content To String#205

Open
misdirection wants to merge 2 commits into
danny-avila:mainfrom
misdirection:fix/assistant-content-string
Open

🪢 fix: Flatten Text-Only Assistant Content To String#205
misdirection wants to merge 2 commits into
danny-avila:mainfrom
misdirection:fix/assistant-content-string

Conversation

@misdirection

Copy link
Copy Markdown

Summary

For non-reasoning replies, formatAssistantMessage emits assistant content as
an array of text parts ([{ type: "text", text: "..." }]). The Chat Completions
spec allows both a string and an array here, so api.openai.com accepts it -
but strict OpenAI-compatible proxies validate literally and only allow a string,
rejecting the array with 400. Since the array first appears when a prior
assistant turn is replayed, this breaks the second turn of a conversation.

Observed against Langdock's OpenAI-compatible EU endpoint:

{"code":"invalid_type","expected":"string","path":["messages",1,"content"],
 "message":"Invalid input: expected string, received array"}

The fix unifies the two tail branches of formatAssistantMessage into a
single-pass buildAssistantContent helper: text-only content is joined into a
string (the canonical, universally valid shape), while content with any non-text
part keeps its array form. This also drops the now-redundant hasReasoning
branch, which did the same flattening - reasoning content is still attached via
createAIMessage. No behavior change for any other input class.

Reproduction

Reproduces only against a strict proxy (real OpenAI returns 200 for both). The
requests differ solely in assistant.content:

// 200 (post-fix)
"messages": [ {user}, {"role":"assistant","content":"Received."}, {user} ]

// 400 against a strict proxy (pre-fix)
"messages": [ {user}, {"role":"assistant","content":[{"type":"text","text":"Received."}]}, {user} ]

Scope

Strict proxies apply the same string-only constraint to the tool (and
function) roles, so a tool returning array/structured content would hit the
same 400. This PR intentionally covers only the assistant role - the
reported and most common case. The same flattening could be extended to those
roles if you'd like; flagging it here rather than widening this change
unprompted.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

  • npx jest src/messages/ - 328 passed, 1 skipped
  • npx tsc --noEmit / npx eslint "src/**/*.ts" - clean
  • Added a regression test for multi-turn text-only content; updated 8 existing
    assertions that encoded the old array shape.
  • Verified end-to-end through LibreChat against Langdock: with the fix
    assistant.content goes out as a string (200); reverting it reproduces the
    array and the 400 (negative control).

Test Configuration

  • Node.js: v22.22.2
  • Package: @librechat/agents@3.1.97

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes

`formatAssistantMessage` emitted assistant `content` as an array of text
parts for non-reasoning replies. The OpenAI Chat Completions schema requires
`assistant.content` to be a string (or null with tool_calls); real OpenAI
tolerates the array form, but strict OpenAI-compatible proxies (Langdock,
LiteLLM, Azure OpenAI schema) reject it with HTTP 400 on multi-turn turns.

Unify both tail branches into a single-pass `buildAssistantContent` helper:
text-only content is joined into a string, mixed content keeps its array
shape. Removes the now-redundant `hasReasoning` branch (reasoning content is
still attached via `createAIMessage`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce5248c2ec

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/messages/format.ts Outdated
`buildAssistantContent` appended `\n` after each text part and ran
`text.trim()` to drop the trailing separator. trim() also stripped
user-visible leading/trailing whitespace from the original reply, so
whitespace-sensitive content (indented code blocks, preformatted output,
trailing newlines) was replayed differently on the next turn — a
regression for non-reasoning replies, which previously kept their
verbatim array shape.

Join the parts with a single `\n` separator instead, leaving the
original text untouched. Whitespace-only parts are already filtered
before this point, so the join never needs trimming. Adds a test
asserting an indented code block with a trailing newline survives the
flatten verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant