fix: return thinking as reasoning output item per OpenAI spec

Thinking content was previously returned as non-standard top-level
fields (thinking, thinking_duration). Now follows the official OpenAI
Responses API format:

- Reasoning appears as a 'type: reasoning' item in the output array
  with summary[].text containing the thinking content
- Message item follows after the reasoning item
- thinking_signature kept as proxy extension (internal multi-turn data)
- Removed ResponseOutput/OutputContent structs in favor of
  serde_json::Value for polymorphic output items
This commit is contained in:
Nikketryhard
2026-02-14 19:16:12 -06:00
parent 7c4e781900
commit 19dc920872
2 changed files with 68 additions and 83 deletions

View File

@@ -72,11 +72,9 @@ struct ResponseData {
status: &'static str,
created_at: u64,
completed_at: Option<u64>,
output: Vec<ResponseOutput>,
output: Vec<serde_json::Value>,
usage: Option<Usage>,
thinking_signature: Option<String>,
thinking: Option<String>,
thinking_duration: Option<String>,
}
/// Build a full Response object matching the official OpenAI schema.
@@ -107,8 +105,6 @@ fn build_response_object(data: ResponseData, params: &RequestParams) -> Response
user: params.user.clone(),
metadata: params.metadata.clone(),
thinking_signature: data.thinking_signature,
thinking: data.thinking,
thinking_duration: data.thinking_duration,
}
}
@@ -339,6 +335,13 @@ async fn handle_responses_sync(
let usage = usage_from_poll(&state.mitm_store, &cascade_id, &poll_result.usage, &params.user_text, &poll_result.text).await;
// Build output array: [reasoning (if present), message]
let mut output_items: Vec<serde_json::Value> = Vec::new();
if let Some(ref thinking_text) = poll_result.thinking {
output_items.push(build_reasoning_output(thinking_text));
}
output_items.push(build_message_output(&msg_id, &poll_result.text));
let resp = build_response_object(
ResponseData {
id: response_id,
@@ -346,21 +349,9 @@ async fn handle_responses_sync(
status: "completed",
created_at,
completed_at: Some(completed_at),
output: vec![ResponseOutput {
output_type: "message",
id: msg_id,
status: "completed",
role: "assistant",
content: vec![OutputContent {
content_type: "output_text",
text: poll_result.text,
annotations: vec![],
}],
}],
output: output_items,
usage: Some(usage),
thinking_signature: poll_result.thinking_signature,
thinking: poll_result.thinking,
thinking_duration: poll_result.thinking_duration,
},
&params,
);
@@ -397,8 +388,6 @@ async fn handle_responses_stream(
output: vec![],
usage: None,
thinking_signature: None,
thinking: None,
thinking_duration: None,
},
&params,
);
@@ -424,20 +413,14 @@ async fn handle_responses_stream(
}),
));
// 3. response.output_item.added
// 3. response.output_item.added (message — reasoning added at completion)
yield Ok(responses_sse_event(
"response.output_item.added",
serde_json::json!({
"type": "response.output_item.added",
"sequence_number": next_seq(),
"output_index": OUTPUT_IDX,
"item": {
"type": "message",
"id": &msg_id,
"status": "in_progress",
"role": "assistant",
"content": [],
}
"item": build_message_output_in_progress(&msg_id),
}),
));
@@ -554,8 +537,6 @@ async fn handle_responses_stream(
output: vec![],
usage: Some(Usage::estimate(&params.user_text, "")),
thinking_signature: None,
thinking: None,
thinking_duration: None,
},
&params,
);
@@ -584,7 +565,7 @@ async fn handle_responses_stream(
/// 1. response.output_text.done
/// 2. response.content_part.done
/// 3. response.output_item.done
/// 4. response.completed
/// 4. response.completed (with reasoning item prepended if present)
#[allow(clippy::too_many_arguments)]
fn completion_events(
resp_id: &str,
@@ -599,22 +580,19 @@ fn completion_events(
params: &RequestParams,
thinking_signature: Option<String>,
thinking: Option<String>,
thinking_duration: Option<String>,
_thinking_duration: Option<String>,
) -> Vec<Event> {
let next_seq = || seq.fetch_add(1, Ordering::Relaxed);
let completed_at = now_unix();
let output_item = serde_json::json!({
"type": "message",
"id": msg_id,
"status": "completed",
"role": "assistant",
"content": [{
"type": "output_text",
"text": text,
"annotations": [],
}],
});
let output_item = build_message_output(msg_id, text);
// Build output array: [reasoning (if present), message]
let mut output_items: Vec<serde_json::Value> = Vec::new();
if let Some(ref thinking_text) = thinking {
output_items.push(build_reasoning_output(thinking_text));
}
output_items.push(build_message_output(msg_id, text));
let completed_resp = build_response_object(
ResponseData {
@@ -623,21 +601,9 @@ fn completion_events(
status: "completed",
created_at,
completed_at: Some(completed_at),
output: vec![ResponseOutput {
output_type: "message",
id: msg_id.to_string(),
status: "completed",
role: "assistant",
content: vec![OutputContent {
content_type: "output_text",
text: text.to_string(),
annotations: vec![],
}],
}],
output: output_items,
usage: Some(usage),
thinking_signature,
thinking,
thinking_duration,
},
params,
);