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]
|
// 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),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user