Qwen Code + Cerebras: every multi-turn request fails with 400 status code (no body)
Summary
When using a Cerebras-hosted model (any model) via the OpenAI-compatible provider, the first turn succeeds but every subsequent turn in the session fails with 400 status code (no body). The CLI's own background subagents (e.g. managed-auto-memory-extractor) fail the same way, since they replay session history.
Environment
- qwen-code 0.23.0 (
@qwen-code/qwen-code npm package)
- Provider config: OpenAI-compatible,
baseUrl: https://api.cerebras.ai/v1, model qwen-3.8-27b (also reproduced with gpt-oss-120b)
- Linux x64
Root cause
The Gemini→OpenAI history converter writes the model's thinking back into request history as an assistant.reasoning_content field:
// packages/core/src/core/openaiContentGenerator/provider/... (converter)
const reasoningContent = reasoningParts.join("");
if (reasoningContent) {
assistantMessage.reasoning_content = reasoningContent;
}
Cerebras returns thinking in a field named reasoning (and accepts reasoning back on input), but rejects reasoning_content on input with HTTP 400 — for every model, not just thinking ones:
{"message": "messages.2.assistant.reasoning_content: property 'messages.2.assistant.reasoning_content' is unsupported",
"type": "invalid_request_error", "param": "validation_error", "code": "wrong_api_format"}
The CLI surfaces this as 400 status code (no body), which hides the actual validation message.
Minimal reproduction
# Turn 1 — succeeds
curl https://api.cerebras.ai/v1/chat/completions \
-H "Authorization: Bearer $CEREBRAS_KEY" -H "Content-Type: application/json" \
-d '{"model":"qwen-3.8-27b","messages":[{"role":"user","content":"test"}]}'
# Turn 2 — 400, exactly what qwen-code sends on the follow-up
curl https://api.cerebras.ai/v1/chat/completions \
-H "Authorization: Bearer $CEREBRAS_KEY" -H "Content-Type: application/json" \
-d '{"model":"qwen-3.8-27b","messages":[
{"role":"user","content":"test"},
{"role":"assistant","content":"Hey! How can I help?","reasoning_content":"The user said test."},
{"role":"user","content":"follow-up question"}
]}'
# → 400 wrong_api_format: messages.2.assistant.reasoning_content unsupported
Note: substituting the field name reasoning (Cerebras's own response field) is accepted on input; only reasoning_content is rejected.
Proposed fix
The codebase already solves this exact problem for Mistral: MistralOpenAICompatibleProvider.buildRequest() maps history through stripReasoningContent(), which deletes reasoning_content from outgoing messages. Cerebras needs the identical treatment — Cerebras endpoints currently fall through to DefaultOpenAICompatibleProvider, which sends the field verbatim.
Add a Cerebras provider (or extend the strip heuristic), following the existing Mistral pattern:
// provider/cerebras.ts
const CEREBRAS_API_HOST = "api.cerebras.ai";
function isCerebrasProvider(config): boolean {
try {
const hostname = new URL(config?.baseUrl ?? "").hostname.toLowerCase();
return hostname === CEREBRAS_API_HOST || hostname.endsWith(`.${CEREBRAS_API_HOST}`);
} catch { return false; }
}
class CerebrasOpenAICompatibleProvider extends DefaultOpenAICompatibleProvider {
buildRequest(request, userPromptId) {
const baseRequest = super.buildRequest(request, userPromptId);
return { ...baseRequest, messages: baseRequest.messages.map(stripReasoningContent) };
}
}
// in determineProvider():
if (isCerebrasProvider(config)) {
return new CerebrasOpenAICompatibleProvider(contentGeneratorConfig, cliConfig);
}
(stripReasoningContent would need to be exported from the Mistral module or moved to a shared util.)
Trade-off to note: stripping thinking from history means the model no longer sees its own prior reasoning across turns. That is already the accepted behavior for Mistral, and is strictly better than every multi-turn request failing. Cerebras accepts reasoning (no _content) on input, so renaming rather than deleting would also work — but deletion matches the existing pattern and is provider-safe.
Verified workaround
Locally patching the installed chunk (~/.local/lib/qwen-code/lib/chunks/chunk-F33GFWPR.js) with the detection + strip above fixes multi-turn sessions against Cerebras — confirmed by resuming a previously failing session and completing follow-up turns with no 400s.
Secondary issue (worth a separate ticket?)
EnhancedErrorHandler reports Cerebras 400s as 400 status code (no body) even though the response does have a JSON body with a precise validation message. Surfacing error.message from the body would have made this diagnosable in seconds instead of requiring session-telemetry archaeology.
Qwen Code + Cerebras: every multi-turn request fails with
400 status code (no body)Summary
When using a Cerebras-hosted model (any model) via the OpenAI-compatible provider, the first turn succeeds but every subsequent turn in the session fails with
400 status code (no body). The CLI's own background subagents (e.g.managed-auto-memory-extractor) fail the same way, since they replay session history.Environment
@qwen-code/qwen-codenpm package)baseUrl: https://api.cerebras.ai/v1, modelqwen-3.8-27b(also reproduced withgpt-oss-120b)Root cause
The Gemini→OpenAI history converter writes the model's thinking back into request history as an
assistant.reasoning_contentfield:Cerebras returns thinking in a field named
reasoning(and acceptsreasoningback on input), but rejectsreasoning_contenton input with HTTP 400 — for every model, not just thinking ones:{"message": "messages.2.assistant.reasoning_content: property 'messages.2.assistant.reasoning_content' is unsupported", "type": "invalid_request_error", "param": "validation_error", "code": "wrong_api_format"}The CLI surfaces this as
400 status code (no body), which hides the actual validation message.Minimal reproduction
Note: substituting the field name
reasoning(Cerebras's own response field) is accepted on input; onlyreasoning_contentis rejected.Proposed fix
The codebase already solves this exact problem for Mistral:
MistralOpenAICompatibleProvider.buildRequest()maps history throughstripReasoningContent(), which deletesreasoning_contentfrom outgoing messages. Cerebras needs the identical treatment — Cerebras endpoints currently fall through toDefaultOpenAICompatibleProvider, which sends the field verbatim.Add a Cerebras provider (or extend the strip heuristic), following the existing Mistral pattern:
(
stripReasoningContentwould need to be exported from the Mistral module or moved to a shared util.)Trade-off to note: stripping thinking from history means the model no longer sees its own prior reasoning across turns. That is already the accepted behavior for Mistral, and is strictly better than every multi-turn request failing. Cerebras accepts
reasoning(no_content) on input, so renaming rather than deleting would also work — but deletion matches the existing pattern and is provider-safe.Verified workaround
Locally patching the installed chunk (
~/.local/lib/qwen-code/lib/chunks/chunk-F33GFWPR.js) with the detection + strip above fixes multi-turn sessions against Cerebras — confirmed by resuming a previously failing session and completing follow-up turns with no 400s.Secondary issue (worth a separate ticket?)
EnhancedErrorHandlerreports Cerebras 400s as400 status code (no body)even though the response does have a JSON body with a precise validation message. Surfacingerror.messagefrom the body would have made this diagnosable in seconds instead of requiring session-telemetry archaeology.