[Bug] Assistant messages whose content is a content-parts array are silently blanked (HTTP 200, content lost)
Component: POST /provider/v1/chat/completions
Endpoint: https://api.commandcode.ai/provider/v1
Model tested: deepseek/deepseek-v4-flash
Client: QwenPaw 2.2.0 (OpenAI-compatible client, AsyncOpenAI/Python 2.33.0)
Severity: High — silent data loss; no error is returned, the caller cannot detect it.
Summary
When a request contains an assistant message whose content is a content-parts array
(e.g. [{"type": "text", "text": "…"}]) instead of a plain string, the message text is
silently dropped by the gateway. The request returns 200 OK and the response looks
normal, but the model behaves as if that assistant turn never existed.
The identical shape is accepted for user / system messages, so this is specific to the
assistant role.
This format is legal per the OpenAI Chat Completions spec — the official openai Python
SDK (v1.88.0, generated from OpenAI's OpenAPI spec) declares:
# openai/types/chat/chat_completion_assistant_message_param.py
ContentArrayOfContentPart: TypeAlias = Union[
ChatCompletionContentPartTextParam,
ChatCompletionContentPartRefusalParam,
]
class ChatCompletionAssistantMessageParam(TypedDict, total=False):
role: Required[Literal["assistant"]]
content: Union[str, Iterable[ContentArrayOfContentPart], None]
Note that text is the only non-refusal part type allowed for assistant — so this is not a
"multimodal" case: the dropped part is plain text.
Minimal reproduction
curl -s https://api.commandcode.ai/provider/v1/chat/completions \
-H "Authorization: Bearer $CCG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v4-flash",
"stream": false,
"messages": [
{"role": "user", "content": "Pick a random 4-digit number. Reply with the number only."},
{"role": "assistant", "content": [{"type": "text", "text": "My number is 7391."}]},
{"role": "user", "content": "What number did you just say? Reply with the number only."}
]
}'
Expected: the model replies 7391.
Actual: the model does not know the number — it asks the user to repeat it, or answers as if
no assistant turn existed.
Control (same request, only content shape changed):
{"role": "assistant", "content": "My number is 7391."}
→ the model replies 7391 correctly.
The payload above is the shape taken from captured production traffic (see Evidence); the A/B
behaviour was verified end-to-end, not only by hand-crafted curl.
Evidence from real traffic
Captured with a transparent local logging reverse proxy placed in front of
https://api.commandcode.ai/provider/v1 (the proxy forwards the body unmodified; it only
records it). Three consecutive experiments, same provider / same model / same task:
| Group |
reasoning_content relay |
content shape on the wire |
Assistant msg present in request |
Model behaviour |
| A |
ON |
array [{"type","text"}] |
✅ present (67 chars) |
❌ "cannot see previous turns" |
| B |
OFF |
array [{"type","text"}] |
✅ present (108 chars) |
❌ still "cannot see previous turn" |
| C |
OFF |
plain string |
✅ present (66 chars) |
✅ chained correctly 5/5 turns |
Group B/C were run after disabling the client's reasoning-relay option, so reasoning_content
was absent from every message — the only remaining variable is the content shape.
Exact wire object (group B, messages[2])
{
"role": "assistant",
"content": [
{"type": "text", "text": "**天空** 🌤️\n\n(接龙规则:…)\n\n要不要继续?…"}
]
}
Request is HTTP 200. The model's own reasoning then says:
"my previous response seems to have been cut off — there's no assistant message shown between the user's first message and second"
"looking at the history, I don't see my previous responses actually recorded (the assistant turns seem missing/empty in this display)"
and one assistant reply even apologises to the user:
「抱歉,前面的回复似乎没有发出去,现在重新接上…」
After the client-side rewrite (content → plain string, group C)
Five consecutive turns with only 「继续接,禁止recall」 (recall explicitly banned, so the model
must rely on its own history):
messages=4 「天空」→ 空调
messages=6 「调」 → 调味
messages=8 「味」 → 味道
messages=10 「道」 → 道路
messages=12 「路」 → …
chain: 天 → 天空 → 空调 → 调味 → 味道 → 道路 → 路
All five correct. Same provider, same model, same request path — only the content encoding
differs.
Why this is a defect rather than "unsupported feature"
user messages with the identical array shape are processed normally in the same
requests, so array support exists — it is the assistant branch that is missing.
- The dropped part is
type: "text" — the one part type the spec allows for assistant.
- If content-parts were intentionally unsupported for
assistant, the correct behaviour is to
reject the request (4xx) or to ignore only the unsupported parts — not to blank the
whole message and return 200 OK.
Impact
Any OpenAI-compatible client that emits content-parts for assistant messages (QwenPaw /
agentscope does this for all roles, including pure-text system prompts) will silently lose
conversation history. Symptoms seen downstream: the model repeatedly claims it cannot see its
own previous replies, re-asks for information the user already received, and calls its own
history-search tools to recover its last output. This is very hard to diagnose because every
HTTP call succeeds and token accounting looks normal.
Suggested fix
Either:
- Support
content as an array for the assistant role (join text parts in order;
refusal parts can be handled or ignored), or
- Reject the request with a clear
400 / 422 explaining that assistant content-parts are
unsupported.
Please do not return 200 while dropping the message.
Appendix — capture bundle
capture/*_request.json — the actual outgoing request bodies (unmodified except header
masking), including the raw JSON above.
capture/requests.jsonl — one line per request: time, model, message roles,
last_assistant_len, last_assistant_has_reasoning_content, rewrite flags.
session_*.redacted.json — QwenPaw session state (structure preserved).
qwenpaw.log.excerpt.txt — server-side log for the same window.
[Bug] Assistant messages whose
contentis a content-parts array are silently blanked (HTTP 200, content lost)Component:
POST /provider/v1/chat/completionsEndpoint:
https://api.commandcode.ai/provider/v1Model tested:
deepseek/deepseek-v4-flashClient: QwenPaw 2.2.0 (OpenAI-compatible client,
AsyncOpenAI/Python 2.33.0)Severity: High — silent data loss; no error is returned, the caller cannot detect it.
Summary
When a request contains an assistant message whose
contentis a content-parts array(e.g.
[{"type": "text", "text": "…"}]) instead of a plain string, the message text issilently dropped by the gateway. The request returns 200 OK and the response looks
normal, but the model behaves as if that assistant turn never existed.
The identical shape is accepted for
user/systemmessages, so this is specific to theassistantrole.This format is legal per the OpenAI Chat Completions spec — the official
openaiPythonSDK (v1.88.0, generated from OpenAI's OpenAPI spec) declares:
Note that
textis the only non-refusal part type allowed forassistant— so this is not a"multimodal" case: the dropped part is plain text.
Minimal reproduction
Expected: the model replies
7391.Actual: the model does not know the number — it asks the user to repeat it, or answers as if
no assistant turn existed.
Control (same request, only
contentshape changed):{"role": "assistant", "content": "My number is 7391."}→ the model replies
7391correctly.The payload above is the shape taken from captured production traffic (see Evidence); the A/B
behaviour was verified end-to-end, not only by hand-crafted curl.
Evidence from real traffic
Captured with a transparent local logging reverse proxy placed in front of
https://api.commandcode.ai/provider/v1(the proxy forwards the body unmodified; it onlyrecords it). Three consecutive experiments, same provider / same model / same task:
reasoning_contentrelaycontentshape on the wire[{"type","text"}][{"type","text"}]Group B/C were run after disabling the client's reasoning-relay option, so
reasoning_contentwas absent from every message — the only remaining variable is the
contentshape.Exact wire object (group B,
messages[2]){ "role": "assistant", "content": [ {"type": "text", "text": "**天空** 🌤️\n\n(接龙规则:…)\n\n要不要继续?…"} ] }Request is
HTTP 200. The model's own reasoning then says:and one assistant reply even apologises to the user:
After the client-side rewrite (
content→ plain string, group C)Five consecutive turns with only 「继续接,禁止recall」 (recall explicitly banned, so the model
must rely on its own history):
All five correct. Same provider, same model, same request path — only the
contentencodingdiffers.
Why this is a defect rather than "unsupported feature"
usermessages with the identical array shape are processed normally in the samerequests, so array support exists — it is the
assistantbranch that is missing.type: "text"— the one part type the spec allows forassistant.assistant, the correct behaviour is toreject the request (4xx) or to ignore only the unsupported parts — not to blank the
whole message and return
200 OK.Impact
Any OpenAI-compatible client that emits content-parts for
assistantmessages (QwenPaw /agentscope does this for all roles, including pure-text system prompts) will silently lose
conversation history. Symptoms seen downstream: the model repeatedly claims it cannot see its
own previous replies, re-asks for information the user already received, and calls its own
history-search tools to recover its last output. This is very hard to diagnose because every
HTTP call succeeds and token accounting looks normal.
Suggested fix
Either:
contentas an array for theassistantrole (jointextparts in order;refusalparts can be handled or ignored), or400/422explaining that assistant content-parts areunsupported.
Please do not return
200while dropping the message.Appendix — capture bundle
capture/*_request.json— the actual outgoing request bodies (unmodified except headermasking), including the raw JSON above.
capture/requests.jsonl— one line per request: time, model, message roles,last_assistant_len,last_assistant_has_reasoning_content, rewrite flags.session_*.redacted.json— QwenPaw session state (structure preserved).qwenpaw.log.excerpt.txt— server-side log for the same window.