feat: emit streaming reasoning events per OpenAI spec

Adds proper streaming SSE events for reasoning content:
- response.output_item.added (reasoning)
- response.reasoning_summary_part.added
- response.reasoning_summary_text.delta
- response.reasoning_summary_text.done
- response.reasoning_summary_part.done
- response.output_item.done (reasoning)

These are emitted before the message events, matching the format
that OpenAI-compatible clients expect for displaying thinking content.
This commit is contained in:
Nikketryhard
2026-02-14 19:57:52 -06:00
parent 5c1f4c77d9
commit b1a089d21d

View File

@@ -620,11 +620,97 @@ fn completion_events(
// Build output array: [reasoning (if present), message]
let mut output_items: Vec<serde_json::Value> = Vec::new();
let mut events: Vec<Event> = Vec::new();
if let Some(ref thinking_text) = thinking {
output_items.push(build_reasoning_output(thinking_text));
let reasoning_item = build_reasoning_output(thinking_text);
let reasoning_id = reasoning_item["id"].as_str().unwrap_or("rs_0").to_string();
output_items.push(reasoning_item.clone());
// ── Reasoning streaming events (OpenAI spec) ──
// 1. response.output_item.added (reasoning item)
events.push(responses_sse_event(
"response.output_item.added",
serde_json::json!({
"type": "response.output_item.added",
"sequence_number": next_seq(),
"output_index": 0,
"item": {
"id": reasoning_id,
"type": "reasoning",
"summary": [],
},
}),
));
// 2. response.reasoning_summary_part.added
events.push(responses_sse_event(
"response.reasoning_summary_part.added",
serde_json::json!({
"type": "response.reasoning_summary_part.added",
"sequence_number": next_seq(),
"item_id": reasoning_id,
"output_index": 0,
"summary_index": 0,
"part": {
"type": "summary_text",
"text": "",
},
}),
));
// 3. response.reasoning_summary_text.delta
events.push(responses_sse_event(
"response.reasoning_summary_text.delta",
serde_json::json!({
"type": "response.reasoning_summary_text.delta",
"sequence_number": next_seq(),
"item_id": reasoning_id,
"output_index": 0,
"summary_index": 0,
"delta": thinking_text,
}),
));
// 4. response.reasoning_summary_text.done
events.push(responses_sse_event(
"response.reasoning_summary_text.done",
serde_json::json!({
"type": "response.reasoning_summary_text.done",
"sequence_number": next_seq(),
"item_id": reasoning_id,
"output_index": 0,
"summary_index": 0,
"text": thinking_text,
}),
));
// 5. response.reasoning_summary_part.done
events.push(responses_sse_event(
"response.reasoning_summary_part.done",
serde_json::json!({
"type": "response.reasoning_summary_part.done",
"sequence_number": next_seq(),
"item_id": reasoning_id,
"output_index": 0,
"summary_index": 0,
"part": {
"type": "summary_text",
"text": thinking_text,
},
}),
));
// 6. response.output_item.done (reasoning item complete)
events.push(responses_sse_event(
"response.output_item.done",
serde_json::json!({
"type": "response.output_item.done",
"sequence_number": next_seq(),
"output_index": 0,
"item": reasoning_item,
}),
));
}
output_items.push(build_message_output(msg_id, text));
let msg_output_index = if thinking.is_some() { 1 } else { 0 };
let completed_resp = build_response_object(
ResponseData {
id: resp_id.to_string(),
@@ -639,26 +725,26 @@ fn completion_events(
params,
);
vec![
// ── Message streaming events ──
// 1. response.output_text.done
responses_sse_event(
events.push(responses_sse_event(
"response.output_text.done",
serde_json::json!({
"type": "response.output_text.done",
"sequence_number": next_seq(),
"item_id": msg_id,
"output_index": out_idx,
"output_index": msg_output_index,
"content_index": content_idx,
"text": text,
}),
),
));
// 2. response.content_part.done
responses_sse_event(
events.push(responses_sse_event(
"response.content_part.done",
serde_json::json!({
"type": "response.content_part.done",
"sequence_number": next_seq(),
"output_index": out_idx,
"output_index": msg_output_index,
"content_index": content_idx,
"part": {
"type": "output_text",
@@ -666,25 +752,26 @@ fn completion_events(
"annotations": [],
},
}),
),
));
// 3. response.output_item.done
responses_sse_event(
events.push(responses_sse_event(
"response.output_item.done",
serde_json::json!({
"type": "response.output_item.done",
"sequence_number": next_seq(),
"output_index": out_idx,
"output_index": msg_output_index,
"item": output_item,
}),
),
));
// 4. response.completed
responses_sse_event(
events.push(responses_sse_event(
"response.completed",
serde_json::json!({
"type": "response.completed",
"sequence_number": next_seq(),
"response": response_to_json(&completed_resp),
}),
),
]
));
events
}