Repository navigation
feat(server): emit a reasoning alias next to reasoning_content on chat completion messages and deltas #1308
Description
Summary
Chat completions surface model thinking only under reasoning_content (the DeepSeek / vLLM spelling), on both the non-streaming message and the streaming delta. A second family of clients (OpenRouter-style consumers, and UIs that follow it) reads reasoning instead and renders nothing for mlxcel. The server already accepts both spellings on input (Message.reasoning with the reasoning_content alias). The change mirrors that on output: every place that writes reasoning_content also writes an identical reasoning field, omitted when absent, behind a server flag that defaults to on.
Current behavior
-
src/server/types/stream.rs:34:pub reasoning_content: Option<String>,
is the only reasoning field on
Delta;ChatCompletionChunk::reasoning_content(src/server/types/stream.rs:163) fills it. -
src/server/types/response.rs:123:ChatMessage.reasoning_content: Option<String>is the only reasoning field on the non-streaming message;with_reasoning_content(src/server/types/response.rs:296) sets it. -
Input side already accepts both:
src/server/types/request.rs:555declares#[serde(alias = "reasoning_content")] pub reasoning: Option<String>and the testmessage_reasoning_field_and_alias_round_trip(src/server/types/request.rs:1245) locks it.
Expected behavior
Non-streaming:
{"role":"assistant","content":"4","reasoning_content":"2+2","reasoning":"2+2"}Streaming delta for a reasoning chunk:
{"delta":{"reasoning_content":"2+","reasoning":"2+"}}Both fields are omitted together when there is no reasoning. The Responses API (response.reasoning_text.delta) and the Anthropic thinking blocks are unchanged. A server flag --reasoning-alias-field <none|reasoning> (env MLXCEL_REASONING_ALIAS_FIELD, default reasoning) lets an operator turn the duplicate off for byte-budget-sensitive deployments.
Implementation plan
src/server/types/stream.rs: add#[serde(skip_serializing_if = "Option::is_none")] pub reasoning: Option<String>toDelta; inChatCompletionChunk::reasoning_contentset both fields from the sametextwhen the alias is enabled. Every other constructor setsreasoning: None.src/server/types/response.rs: add the same field toChatMessage;with_reasoning_contentfills both.src/server/config.rs: addpub reasoning_alias_field: ReasoningAliasField(enum { None, Reasoning }, defaultReasoning); plumb fromsrc/bin/mlx_server.rsandsrc/server/cli_input.rslikeenable_props_endpoint. Thread the flag into the two constructors above through their callers insrc/server/routes/chat.rs(both the streaming reasoning emit and the non-streamingwith_reasoning_contentsite) andsrc/server/router_front.rs(the disaggregated front emits the same chunks atemit_filtered).- Docs: note the alias in the chat-completions section of the server docs and in
docs/responses-api.mdwherereasoning_contentis described.
Validation
(a) Unit tests:
src/server/streaming_tests.rs:reasoning_chunk_carries_both_reasoning_fields(serialize a reasoning chunk, assert both keys present and equal; assert a content chunk has neither).src/server/types/response.rstests:with_reasoning_content_sets_alias_fieldandmessage_without_reasoning_omits_both_fields.src/server/config_tests.rs:reasoning_alias_field_none_suppresses_alias.
(b) Real checkpoint: mlx-community/Qwen3-4B-4bit.
./target/release/mlxcel-server -m models/qwen3-4b-4bit --port 8080
curl -sN localhost:8080/v1/chat/completions -H 'content-type: application/json' -d '{"model":"q","messages":[{"role":"user","content":"What is 17*23? Think briefly."}],"stream":true,"max_tokens":128}' | grep -c '"reasoning":'Acceptance: the count is greater than zero and equals the number of chunks that carry reasoning_content; the non-streaming variant returns a message where .reasoning == .reasoning_content; with --reasoning-alias-field none the count is zero.
Acceptance criteria
- Streaming deltas and non-streaming messages carry
reasoningalongsidereasoning_content, omitted together when absent. -
--reasoning-alias-field nonesuppresses the alias. - Unit tests above added; real-checkpoint check passes.
- cargo test --workspace --profile test-fast --features metal,accelerate passes
- cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --all -- --check pass
Out of scope
- Emitting
reasoning_detailsarrays or signature fields. - Changing the Responses or Anthropic reasoning surfaces.
Activity
feat: add configurable reasoning response alias (lablup#1508)