🪢 fix: Flatten Text-Only Assistant Content To String#205
Open
misdirection wants to merge 2 commits into
Open
Conversation
`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>
There was a problem hiding this comment.
💡 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".
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
For non-reasoning replies,
formatAssistantMessageemits assistantcontentasan array of text parts (
[{ type: "text", text: "..." }]). The Chat Completionsspec allows both a string and an array here, so
api.openai.comaccepts 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 priorassistant 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
formatAssistantMessageinto asingle-pass
buildAssistantContenthelper: text-only content is joined into astring (the canonical, universally valid shape), while content with any non-text
part keeps its array form. This also drops the now-redundant
hasReasoningbranch, 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
200for both). Therequests differ solely in
assistant.content:Scope
Strict proxies apply the same string-only constraint to the
tool(andfunction) roles, so a tool returning array/structured content would hit thesame
400. This PR intentionally covers only theassistantrole - thereported 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
Testing
npx jest src/messages/- 328 passed, 1 skippednpx tsc --noEmit/npx eslint "src/**/*.ts"- cleanassertions that encoded the old array shape.
assistant.contentgoes out as a string (200); reverting it reproduces thearray and the
400(negative control).Test Configuration
@librechat/agents@3.1.97Checklist