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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user