Skip to content

Conversation

@KrishnanPrash
Copy link
Contributor

@KrishnanPrash KrishnanPrash commented Sep 16, 2025

Overview:

This PR addresses an issue where Dynamo's OpenAI-compatible API fails when receiving messages with multi-part content arrays. This failure occurs because the underlying chat (jinja) template expects messages[content] to be a string, but a list is provided.

Example Request:

curl localhost:8000/v1/chat/completions   -H "Content-Type: application/json"   -d '{
    "model": "Qwen/Qwen2.5-0.5B-Instruct",
    "messages": [
    {
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "..."
          }, 
          {
            "type": "text", 
            "text": "..."
          } 
        ]
    }
    ],
    "stream":false,
    "max_tokens": 300
  }'

Output:

{
  "error": "Failed to generate completions: invalid operation: tried to use + operator on unsupported types string and sequence (in default:1)"
}

Details:

For text models, if a list is provided for messages[content], we concatenate all the text provided into a single string to match the expectations of the chat template.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of multi-part message content by consolidating text parts into readable messages while preserving non-text and empty arrays. This reduces formatting glitches and improves compatibility with OpenAI-style chat payloads.
  • Tests

    • Added unit tests for multipart user content, mixed-role messages, empty arrays, and single-string content to ensure consistent behavior and prevent regressions.

Signed-off-by: Krishnan Prashanth <[email protected]>
@KrishnanPrash KrishnanPrash requested a review from a team as a code owner September 16, 2025 19:31
@copy-pr-bot
Copy link

copy-pr-bot bot commented Sep 16, 2025

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 16, 2025

Walkthrough

Introduces a private helper to collapse array-based message content into newline-joined text. Updates NvCreateChatCompletionRequest::messages() to conditionally apply this collapse when any message content is an array. Adds unit tests covering multipart text collapsing, mixed message handling, empty arrays preservation, and unchanged single-string content. No public API changes.

Changes

Cohort / File(s) Summary
Prompt preprocessing and tests
lib/llm/src/preprocessor/prompt/template/oai.rs
Added may_be_fix_msg_content to collapse array-based content into strings; updated messages() to auto-collapse when needed; added unit tests for multipart, mixed, empty-array, and single-text scenarios; no public API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Caller
  participant R as NvCreateChatCompletionRequest
  participant S as Serializer
  participant F as may_be_fix_msg_content
  participant H as HTTP Client

  C->>R: build request (messages)
  R->>S: serialize inner messages -> Value
  alt any content is array
    R->>F: collapse text parts
    F-->>R: Value with collapsed content
  else
    R-->>R: use serialized messages as-is
  end
  R->>H: send request payload
  H-->>C: response
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I stitched the parts with tidy care,
From shards of text to newline air—
A gentle hop through fields of bytes,
Collapsing crumbs to readable lights.
Now messages sing in single streams,
While tests keep watch on bunny dreams. 🐇✨

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: Convert message[content] from list to string." succinctly and accurately summarizes the primary change in this PR — collapsing multipart message content arrays into a single string. It directly reflects the PR objective and the implementation described in the summary, and is concise and clear for reviewers scanning history. The "feat:" prefix is appropriate for this behavioral change.
Description Check ✅ Passed The PR description is mostly complete: it contains a clear Overview with a reproducible example and error, a Details section explaining the fix, and a Related Issues entry closing #2874. It omits the repository template's "Where should the reviewer start?" section which should call out the key files to review. Because the missing section is a minor omission and the description otherwise documents the problem, reproduction, and resolution well, the check passes.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/llm/src/preprocessor/prompt/template/oai.rs (2)

142-160: Tighten failure mode and keep behavior localized.

  • Prefer expect(...) over unwrap() for clearer diagnostics.
  • Optional: let the helper decide whether to change anything and remove the pre‑scan to avoid double traversal (minor).
-        let messages_json = serde_json::to_value(&self.inner.messages).unwrap();
+        let messages_json = serde_json::to_value(&self.inner.messages)
+            .expect("serializing chat messages to JSON should not fail");

If you want to simplify:

-        let needs_collapse = if let Some(arr) = messages_json.as_array() {
-            arr.iter().any(|msg| {
-                msg.get("content")
-                    .and_then(|c| c.as_array())
-                    .is_some()
-            })
-        } else {
-            false
-        };
-
-        if needs_collapse {
-            may_be_fix_msg_content(messages_json)
-        } else {
-            Value::from_serialize(&messages_json)
-        }
+        may_be_fix_msg_content(messages_json)

410-487: Add a test for mixed content (text + image) to prevent regressions.

Ensure we don’t collapse when non‑text parts are present.

@@
     fn test_may_be_fix_msg_content_mixed_messages() {
@@
     }
+
+    #[test]
+    fn test_may_be_fix_msg_content_text_and_image_preserved() {
+        let json_str = r#"{
+            "model": "gpt-4o",
+            "messages": [
+                {
+                    "role": "user",
+                    "content": [
+                        {"type": "text", "text": "Caption:"},
+                        {"type": "image_url", "image_url": {"url": "https://example.com/cat.png"}}
+                    ]
+                }
+            ]
+        }"#;
+
+        let request: NvCreateChatCompletionRequest = serde_json::from_str(json_str).unwrap();
+        let messages = serde_json::to_value(request.messages()).unwrap();
+
+        assert!(messages[0]["content"].is_array(), "Mixed parts must not be collapsed");
+        let parts = messages[0]["content"].as_array().unwrap();
+        assert_eq!(parts.len(), 2);
+        assert_eq!(parts[0]["type"], "text");
+        assert_eq!(parts[1]["type"], "image_url");
+    }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87e6e05 and e3eff41.

📒 Files selected for processing (1)
  • lib/llm/src/preprocessor/prompt/template/oai.rs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
lib/llm/src/preprocessor/prompt/template/oai.rs (1)
lib/llm/src/preprocessor/prompt.rs (2)
  • messages (62-62)
  • model (61-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build and Test - dynamo
  • GitHub Check: pre-merge-rust (lib/bindings/python)
  • GitHub Check: pre-merge-rust (lib/runtime/examples)
  • GitHub Check: pre-merge-rust (.)
🔇 Additional comments (2)
lib/llm/src/preprocessor/prompt/template/oai.rs (2)

509-528: LGTM: single-string content remains unchanged.


491-507: ```shell
#!/bin/bash
set -euo pipefail

echo "=== lib/llm/src/preprocessor/prompt/template/oai.rs (lines 480-520) ==="
if [ -f lib/llm/src/preprocessor/prompt/template/oai.rs ]; then
sed -n '480,520p' lib/llm/src/preprocessor/prompt/template/oai.rs || true
else
echo "file not found: lib/llm/src/preprocessor/prompt/template/oai.rs"
fi

echo
echo "=== Search for template-related crates/usages (minijinja/jinja) ==="
rg -n --hidden --follow -S 'minijinja|jinja' || true

echo
echo "=== Find common template files by glob ==="
fd -t f -g '.j2' -g '.jinja' -g '.jinja2' -g '.tmpl' -g '.template' -g '.jinja.html' -g '*.jinja2.html' . || true

echo
echo "=== Search for 'content' inside Jinja-like braces across repo ==="
rg -n --hidden --follow -S '{{[^}]\bcontent\b[^}]}}' || true

echo
echo "=== Search for '+' concatenation with 'content' anywhere ==="
rg -n --hidden --follow -S '+\scontent|content\s+' || true

echo
echo "=== Broader search for 'content' occurrences (showing context) ==="
rg -n --hidden --follow -C2 '\bcontent\b' || true

echo
echo "=== Done ==="


</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Signed-off-by: Krishnan Prashanth <[email protected]>
@grahamking
Copy link
Contributor

grahamking commented Sep 16, 2025

When have you seen this happen, these multiple text content blocks?

Solution sounds good to me, but I'm curious what kind of clients do this.

@KrishnanPrash
Copy link
Contributor Author

When have you seen this happen, these multiple text content blocks?

@ayushag-nv might be able to provide more insight into this. Additionally, this GitHub Issue (#2874) shows an inference request that passes in message[content] as a list of one element.

Additionally, collapsing message[content] is currently what vLLM and SGLang are doing under the hood to maintain compatibility with chat templates that expect strings. At minimum, this PR should help us maintain feature parity.

cc: @grahamking

Signed-off-by: Krishnan Prashanth <[email protected]>
@ayushag-nv
Copy link
Contributor

@KrishnanPrash Can you validate this as well.

# # messages = [
# #     {
# #         "role": "user",
# #         "content": [
# #             {"type": "text", "text": "Pin A description. image_sig: f9c2d8a53398c02ae9d561ac0c8275dc"},
# #             {"type": "text", "text": "instruction: rose cardigan"}
# #         ]

# #     },
# # ]

@KrishnanPrash KrishnanPrash self-assigned this Sep 16, 2025
… multi-content, added docstrings

Signed-off-by: Krishnan Prashanth <[email protected]>
Copy link
Contributor

@ayushag-nv ayushag-nv left a comment

Choose a reason for hiding this comment

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

lgtm !

@KrishnanPrash KrishnanPrash enabled auto-merge (squash) September 19, 2025 00:01
@KrishnanPrash
Copy link
Contributor Author

/ok to test 9b69274

@rmccorm4 rmccorm4 disabled auto-merge September 19, 2025 15:46
@rmccorm4 rmccorm4 enabled auto-merge (squash) September 19, 2025 15:46
@rmccorm4 rmccorm4 merged commit 5abea1b into main Sep 19, 2025
13 checks passed
@rmccorm4 rmccorm4 deleted the kprashanth/multi_content_msg branch September 19, 2025 16:22
hhzhang16 pushed a commit that referenced this pull request Sep 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants