@@ -602,7 +602,10 @@ pub fn into_open_ai_response(
} else {
None
},
- reasoning: reasoning_effort.map(|effort| open_ai::responses::ReasoningConfig { effort }),
+ reasoning: reasoning_effort.map(|effort| open_ai::responses::ReasoningConfig {
+ effort,
+ summary: Some(open_ai::responses::ReasoningSummaryMode::Auto),
+ }),
}
}
@@ -963,10 +966,20 @@ impl OpenAiResponseEventMapper {
self.function_calls_by_item.insert(item_id, entry);
}
}
- ResponseOutputItem::Unknown => {}
+ ResponseOutputItem::Reasoning(_) | ResponseOutputItem::Unknown => {}
}
events
}
+ ResponsesStreamEvent::ReasoningSummaryTextDelta { delta, .. } => {
+ if delta.is_empty() {
+ Vec::new()
+ } else {
+ vec![Ok(LanguageModelCompletionEvent::Thinking {
+ text: delta,
+ signature: None,
+ })]
+ }
+ }
ResponsesStreamEvent::OutputTextDelta { delta, .. } => {
if delta.is_empty() {
Vec::new()
@@ -1075,10 +1088,22 @@ impl OpenAiResponseEventMapper {
error.message
)))]
}
- ResponsesStreamEvent::OutputTextDone { .. } => Vec::new(),
- ResponsesStreamEvent::OutputItemDone { .. }
+ ResponsesStreamEvent::ReasoningSummaryPartAdded { summary_index, .. } => {
+ if summary_index > 0 {
+ vec![Ok(LanguageModelCompletionEvent::Thinking {
+ text: "\n\n".to_string(),
+ signature: None,
+ })]
+ } else {
+ Vec::new()
+ }
+ }
+ ResponsesStreamEvent::OutputTextDone { .. }
+ | ResponsesStreamEvent::OutputItemDone { .. }
| ResponsesStreamEvent::ContentPartAdded { .. }
| ResponsesStreamEvent::ContentPartDone { .. }
+ | ResponsesStreamEvent::ReasoningSummaryTextDone { .. }
+ | ResponsesStreamEvent::ReasoningSummaryPartDone { .. }
| ResponsesStreamEvent::Created { .. }
| ResponsesStreamEvent::InProgress { .. }
| ResponsesStreamEvent::Unknown => Vec::new(),
@@ -1416,8 +1441,9 @@ mod tests {
use gpui::TestAppContext;
use language_model::{LanguageModelRequestMessage, LanguageModelRequestTool};
use open_ai::responses::{
- ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage, ResponseStatusDetails,
- ResponseSummary, ResponseUsage, StreamEvent as ResponsesStreamEvent,
+ ReasoningSummaryPart, ResponseFunctionToolCall, ResponseOutputItem, ResponseOutputMessage,
+ ResponseReasoningItem, ResponseStatusDetails, ResponseSummary, ResponseUsage,
+ StreamEvent as ResponsesStreamEvent,
};
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -1675,7 +1701,7 @@ mod tests {
}
],
"prompt_cache_key": "thread-123",
- "reasoning": { "effort": "low" }
+ "reasoning": { "effort": "low", "summary": "auto" }
});
assert_eq!(serialized, expected);
@@ -2114,4 +2140,166 @@ mod tests {
})
));
}
+
+ #[test]
+ fn responses_stream_maps_reasoning_summary_deltas() {
+ let events = vec![
+ ResponsesStreamEvent::OutputItemAdded {
+ output_index: 0,
+ sequence_number: None,
+ item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
+ id: Some("rs_123".into()),
+ summary: vec![],
+ }),
+ },
+ ResponsesStreamEvent::ReasoningSummaryPartAdded {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ summary_index: 0,
+ },
+ ResponsesStreamEvent::ReasoningSummaryTextDelta {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ delta: "Thinking about".into(),
+ },
+ ResponsesStreamEvent::ReasoningSummaryTextDelta {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ delta: " the answer".into(),
+ },
+ ResponsesStreamEvent::ReasoningSummaryTextDone {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ text: "Thinking about the answer".into(),
+ },
+ ResponsesStreamEvent::ReasoningSummaryPartDone {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ summary_index: 0,
+ },
+ ResponsesStreamEvent::ReasoningSummaryPartAdded {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ summary_index: 1,
+ },
+ ResponsesStreamEvent::ReasoningSummaryTextDelta {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ delta: "Second part".into(),
+ },
+ ResponsesStreamEvent::ReasoningSummaryTextDone {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ text: "Second part".into(),
+ },
+ ResponsesStreamEvent::ReasoningSummaryPartDone {
+ item_id: "rs_123".into(),
+ output_index: 0,
+ summary_index: 1,
+ },
+ ResponsesStreamEvent::OutputItemDone {
+ output_index: 0,
+ sequence_number: None,
+ item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
+ id: Some("rs_123".into()),
+ summary: vec![
+ ReasoningSummaryPart::SummaryText {
+ text: "Thinking about the answer".into(),
+ },
+ ReasoningSummaryPart::SummaryText {
+ text: "Second part".into(),
+ },
+ ],
+ }),
+ },
+ ResponsesStreamEvent::OutputItemAdded {
+ output_index: 1,
+ sequence_number: None,
+ item: response_item_message("msg_456"),
+ },
+ ResponsesStreamEvent::OutputTextDelta {
+ item_id: "msg_456".into(),
+ output_index: 1,
+ content_index: Some(0),
+ delta: "The answer is 42".into(),
+ },
+ ResponsesStreamEvent::Completed {
+ response: ResponseSummary::default(),
+ },
+ ];
+
+ let mapped = map_response_events(events);
+
+ let thinking_events: Vec<_> = mapped
+ .iter()
+ .filter(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. }))
+ .collect();
+ assert_eq!(
+ thinking_events.len(),
+ 4,
+ "expected 4 thinking events (2 deltas + separator + second delta), got {:?}",
+ thinking_events,
+ );
+
+ assert!(matches!(
+ &thinking_events[0],
+ LanguageModelCompletionEvent::Thinking { text, .. } if text == "Thinking about"
+ ));
+ assert!(matches!(
+ &thinking_events[1],
+ LanguageModelCompletionEvent::Thinking { text, .. } if text == " the answer"
+ ));
+ assert!(
+ matches!(
+ &thinking_events[2],
+ LanguageModelCompletionEvent::Thinking { text, .. } if text == "\n\n"
+ ),
+ "expected separator between summary parts"
+ );
+ assert!(matches!(
+ &thinking_events[3],
+ LanguageModelCompletionEvent::Thinking { text, .. } if text == "Second part"
+ ));
+
+ assert!(mapped.iter().any(|e| matches!(
+ e,
+ LanguageModelCompletionEvent::Text(t) if t == "The answer is 42"
+ )));
+ }
+
+ #[test]
+ fn responses_stream_maps_reasoning_from_done_only() {
+ let events = vec![
+ ResponsesStreamEvent::OutputItemAdded {
+ output_index: 0,
+ sequence_number: None,
+ item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
+ id: Some("rs_789".into()),
+ summary: vec![],
+ }),
+ },
+ ResponsesStreamEvent::OutputItemDone {
+ output_index: 0,
+ sequence_number: None,
+ item: ResponseOutputItem::Reasoning(ResponseReasoningItem {
+ id: Some("rs_789".into()),
+ summary: vec![ReasoningSummaryPart::SummaryText {
+ text: "Summary without deltas".into(),
+ }],
+ }),
+ },
+ ResponsesStreamEvent::Completed {
+ response: ResponseSummary::default(),
+ },
+ ];
+
+ let mapped = map_response_events(events);
+
+ assert!(
+ !mapped
+ .iter()
+ .any(|e| matches!(e, LanguageModelCompletionEvent::Thinking { .. })),
+ "OutputItemDone reasoning should not produce Thinking events (no delta/done text events)"
+ );
+ }
}
@@ -78,6 +78,16 @@ pub enum ResponseInputContent {
#[derive(Serialize, Debug)]
pub struct ReasoningConfig {
pub effort: ReasoningEffort,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub summary: Option<ReasoningSummaryMode>,
+}
+
+#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ReasoningSummaryMode {
+ Auto,
+ Concise,
+ Detailed,
}
#[derive(Serialize, Debug)]
@@ -150,6 +160,30 @@ pub enum StreamEvent {
content_index: Option<usize>,
text: String,
},
+ #[serde(rename = "response.reasoning_summary_part.added")]
+ ReasoningSummaryPartAdded {
+ item_id: String,
+ output_index: usize,
+ summary_index: usize,
+ },
+ #[serde(rename = "response.reasoning_summary_text.delta")]
+ ReasoningSummaryTextDelta {
+ item_id: String,
+ output_index: usize,
+ delta: String,
+ },
+ #[serde(rename = "response.reasoning_summary_text.done")]
+ ReasoningSummaryTextDone {
+ item_id: String,
+ output_index: usize,
+ text: String,
+ },
+ #[serde(rename = "response.reasoning_summary_part.done")]
+ ReasoningSummaryPartDone {
+ item_id: String,
+ output_index: usize,
+ summary_index: usize,
+ },
#[serde(rename = "response.function_call_arguments.delta")]
FunctionCallArgumentsDelta {
item_id: String,
@@ -219,6 +253,25 @@ pub struct ResponseUsage {
pub enum ResponseOutputItem {
Message(ResponseOutputMessage),
FunctionCall(ResponseFunctionToolCall),
+ Reasoning(ResponseReasoningItem),
+ #[serde(other)]
+ Unknown,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct ResponseReasoningItem {
+ #[serde(default)]
+ pub id: Option<String>,
+ #[serde(default)]
+ pub summary: Vec<ReasoningSummaryPart>,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum ReasoningSummaryPart {
+ SummaryText {
+ text: String,
+ },
#[serde(other)]
Unknown,
}
@@ -356,6 +409,21 @@ pub async fn stream_response(
});
}
}
+ ResponseOutputItem::Reasoning(reasoning) => {
+ if let Some(ref item_id) = reasoning.id {
+ for part in &reasoning.summary {
+ if let ReasoningSummaryPart::SummaryText { text } = part {
+ all_events.push(
+ StreamEvent::ReasoningSummaryTextDelta {
+ item_id: item_id.clone(),
+ output_index,
+ delta: text.clone(),
+ },
+ );
+ }
+ }
+ }
+ }
ResponseOutputItem::Unknown => {}
}