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

@@ -1045,7 +1045,112 @@ def test_openai_nonstream_branch_unit(monkeypatch):
res = _run(inspect.unwrap(module.openai_chat_completions)("chat-1"))
assert res["choices"][0]["message"]["content"] == "world"
@pytest.mark.p2
def test_openai_defaults_to_nonstream_when_stream_omitted_unit(monkeypatch):
"""Omitted stream must default to false (OpenAI API compat), not SSE."""
module = _load_openai_api_module(monkeypatch)
monkeypatch.setattr(module, "num_tokens_from_string", lambda text: len(text or ""))
monkeypatch.setattr(
module.DialogService,
"query",
lambda **_kwargs: [SimpleNamespace(kb_ids=[], llm_id="chat-model", tenant_id="tenant-1")],
)
stream_flags = []
async def fake_async_chat(_dia, _msg, stream, **_kwargs):
stream_flags.append(stream)
yield {"answer": "hello", "reference": {}}
monkeypatch.setattr(module, "async_chat", fake_async_chat)
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"model": "model",
"messages": [{"role": "user", "content": "hi"}],
}
),
)
res = _run(inspect.unwrap(module.openai_chat_completions)("chat-1"))
assert stream_flags == [False]
assert isinstance(res, dict)
assert res["object"] == "chat.completion"
assert res["choices"][0]["message"]["content"] == "hello"
@pytest.mark.p2
def test_openai_array_text_content_normalized_unit(monkeypatch):
"""OpenAI-style array content with text parts must not crash async_chat."""
module = _load_openai_api_module(monkeypatch)
monkeypatch.setattr(module, "num_tokens_from_string", lambda text: len(text or ""))
monkeypatch.setattr(
module.DialogService,
"query",
lambda **_kwargs: [SimpleNamespace(kb_ids=[], llm_id="chat-model", tenant_id="tenant-1")],
)
captured_msg = []
async def fake_async_chat(_dia, msg, _stream, **_kwargs):
captured_msg.append(msg)
yield {"answer": "ok", "reference": {}}
monkeypatch.setattr(module, "async_chat", fake_async_chat)
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"model": "model",
"stream": False,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Hello"},
{"type": "text", "text": "World"},
],
}
],
}
),
)
res = _run(inspect.unwrap(module.openai_chat_completions)("chat-1"))
assert captured_msg[0][0]["content"] == "Hello\nWorld"
assert res["choices"][0]["message"]["content"] == "ok"
@pytest.mark.p2
def test_openai_invalid_message_content_type_unit(monkeypatch):
module = _load_openai_api_module(monkeypatch)
monkeypatch.setattr(
module.DialogService,
"query",
lambda **_kwargs: [SimpleNamespace(kb_ids=[], llm_id="chat-model", tenant_id="tenant-1")],
)
monkeypatch.setattr(
module,
"get_request_json",
lambda: _AwaitableValue(
{
"model": "model",
"messages": [{"role": "user", "content": 12345}],
}
),
)
res = _run(inspect.unwrap(module.openai_chat_completions)("chat-1"))
assert "messages[].content must be a string or an array of content parts." in res["message"]
@pytest.mark.p2
def test_agents_openai_compatibility_unit(monkeypatch):