fix(api): handle array message content on OpenAI chat completions (#15359)

### Related issues

Closes #15358

<!-- After filing upstream, replace XXXX with your issue number. -->

---

### What problem does this PR solve?

`POST /api/v1/openai/<chat_id>/chat/completions` forwards `messages` to
`async_chat` without normalizing `content`. Downstream, `dialog_service`
assumes string content:

```python
re.sub(r"##\d+\$\$", "", m["content"])
```

OpenAI-compatible clients may send `content` as an **array** of parts
(text, `image_url`, etc.), including text-only arrays. That causes
`TypeError` and HTTP **500** instead of a valid response or a clear
**400**.

`openai_api.py` also reads `messages[-1]["content"]` directly for
`prompt` without handling list-shaped content.

This PR normalizes array `content` to a string (concatenating `type:
text` parts) before calling `async_chat`, matching a minimal
OpenAI-compat path. Image parts can be documented as unsupported or
handled in a follow-up if vision integration is required.
This commit is contained in:
Hernandez Avelino
2026-06-01 19:27:03 -07:00
committed by GitHub
parent 67a3ed7558
commit 09d0a17453
2 changed files with 146 additions and 1 deletions

View File

@@ -91,6 +91,43 @@ def _build_sse_response(body):
return resp
def _normalize_message_content(content):
"""Convert OpenAI message content to a string for the dialog layer.
Supports string content and array parts with ``type: text``. Other part types
(e.g. image_url) are ignored until vision is wired through this route.
"""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
text = part.get("text", "")
if text is not None:
parts.append(str(text))
return "\n".join(parts)
return None
def _normalize_openai_messages(messages):
"""Return (normalized_messages, error_message). error_message is set on failure."""
if not isinstance(messages, list):
return None, "messages must be an array."
normalized = []
for message in messages:
if not isinstance(message, dict):
return None, "Each message must be an object."
content = _normalize_message_content(message.get("content"))
if content is None:
return None, "messages[].content must be a string or an array of content parts."
normalized.append({**message, "content": content})
return normalized, None
@manager.route("/openai/<chat_id>/chat/completions", methods=["POST"]) # noqa: F821
@login_required
@validate_request("model", "messages")
@@ -113,6 +150,9 @@ async def openai_chat_completions(chat_id):
messages = req.get("messages", [])
if len(messages) < 1:
return get_error_data_result("You have to provide messages.")
messages, normalize_error = _normalize_openai_messages(messages)
if normalize_error:
return get_error_data_result(normalize_error)
if messages[-1]["role"] != "user":
return get_error_data_result("The last content of this conversation is not from user.")