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] // Build output array: [reasoning (if present), message]
let mut output_items: Vec<serde_json::Value> = Vec::new(); let mut output_items: Vec<serde_json::Value> = Vec::new();
let mut events: Vec<Event> = Vec::new();
if let Some(ref thinking_text) = thinking { 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)); 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( let completed_resp = build_response_object(
ResponseData { ResponseData {
id: resp_id.to_string(), id: resp_id.to_string(),
@@ -639,52 +725,53 @@ fn completion_events(
params, params,
); );
vec![ // ── Message streaming events ──
// 1. response.output_text.done // 1. response.output_text.done
responses_sse_event( events.push(responses_sse_event(
"response.output_text.done", "response.output_text.done",
serde_json::json!({ serde_json::json!({
"type": "response.output_text.done", "type": "response.output_text.done",
"sequence_number": next_seq(), "sequence_number": next_seq(),
"item_id": msg_id, "item_id": msg_id,
"output_index": out_idx, "output_index": msg_output_index,
"content_index": content_idx, "content_index": content_idx,
"text": text,
}),
));
// 2. response.content_part.done
events.push(responses_sse_event(
"response.content_part.done",
serde_json::json!({
"type": "response.content_part.done",
"sequence_number": next_seq(),
"output_index": msg_output_index,
"content_index": content_idx,
"part": {
"type": "output_text",
"text": text, "text": text,
}), "annotations": [],
), },
// 2. response.content_part.done }),
responses_sse_event( ));
"response.content_part.done", // 3. response.output_item.done
serde_json::json!({ events.push(responses_sse_event(
"type": "response.content_part.done", "response.output_item.done",
"sequence_number": next_seq(), serde_json::json!({
"output_index": out_idx, "type": "response.output_item.done",
"content_index": content_idx, "sequence_number": next_seq(),
"part": { "output_index": msg_output_index,
"type": "output_text", "item": output_item,
"text": text, }),
"annotations": [], ));
}, // 4. response.completed
}), events.push(responses_sse_event(
), "response.completed",
// 3. response.output_item.done serde_json::json!({
responses_sse_event( "type": "response.completed",
"response.output_item.done", "sequence_number": next_seq(),
serde_json::json!({ "response": response_to_json(&completed_resp),
"type": "response.output_item.done", }),
"sequence_number": next_seq(), ));
"output_index": out_idx,
"item": output_item, events
}),
),
// 4. response.completed
responses_sse_event(
"response.completed",
serde_json::json!({
"type": "response.completed",
"sequence_number": next_seq(),
"response": response_to_json(&completed_resp),
}),
),
]
} }