diff --git a/src/api/responses.rs b/src/api/responses.rs index 206d58f..90cfe88 100644 --- a/src/api/responses.rs +++ b/src/api/responses.rs @@ -72,11 +72,9 @@ struct ResponseData { status: &'static str, created_at: u64, completed_at: Option, - output: Vec, + output: Vec, usage: Option, thinking_signature: Option, - thinking: Option, - thinking_duration: Option, } /// 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, ¶ms.user_text, &poll_result.text).await; + // Build output array: [reasoning (if present), message] + let mut output_items: Vec = 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, }, ¶ms, ); @@ -397,8 +388,6 @@ async fn handle_responses_stream( output: vec![], usage: None, thinking_signature: None, - thinking: None, - thinking_duration: None, }, ¶ms, ); @@ -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(¶ms.user_text, "")), thinking_signature: None, - thinking: None, - thinking_duration: None, }, ¶ms, ); @@ -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, thinking: Option, - thinking_duration: Option, + _thinking_duration: Option, ) -> Vec { 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 = 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, ); diff --git a/src/api/types.rs b/src/api/types.rs index 328d670..d195434 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -76,7 +76,9 @@ pub(crate) struct ResponsesResponse { #[serde(serialize_with = "serialize_option_u64")] pub max_output_tokens: Option, pub model: String, - pub output: Vec, + /// Output items — can contain both `"reasoning"` and `"message"` items. + /// Uses serde_json::Value because reasoning and message items have different shapes. + pub output: Vec, pub parallel_tool_calls: bool, pub previous_response_id: Option, pub reasoning: Reasoning, @@ -91,34 +93,10 @@ pub(crate) struct ResponsesResponse { pub user: Option, pub metadata: serde_json::Value, /// Proxy extension: opaque thinking verification signature. - /// Present for all models. Required for multi-turn chaining with thinking models. + /// Required for multi-turn chaining with thinking models. + /// Not part of the official OpenAI spec — internal proxy data. #[serde(skip_serializing_if = "Option::is_none")] pub thinking_signature: Option, - /// Proxy extension: the model's internal reasoning/thinking content. - /// Available for all models (Opus, Gemini Flash, Gemini Pro). - #[serde(skip_serializing_if = "Option::is_none")] - pub thinking: Option, - /// Proxy extension: time spent thinking (e.g. "0.041999832s"). - #[serde(skip_serializing_if = "Option::is_none")] - pub thinking_duration: Option, -} - -#[derive(Serialize, Clone)] -pub(crate) struct ResponseOutput { - #[serde(rename = "type")] - pub output_type: &'static str, - pub id: String, - pub status: &'static str, - pub role: &'static str, - pub content: Vec, -} - -#[derive(Serialize, Clone)] -pub(crate) struct OutputContent { - #[serde(rename = "type")] - pub content_type: &'static str, - pub text: String, - pub annotations: Vec, } #[derive(Serialize, Clone)] @@ -201,6 +179,47 @@ impl Default for TextFormat { } } +// ─── Output item builders ──────────────────────────────────────────────────── + +/// Build a reasoning output item (goes BEFORE the message item in `output`). +/// Matches: https://platform.openai.com/docs/api-reference/responses +pub fn build_reasoning_output(thinking_text: &str) -> serde_json::Value { + serde_json::json!({ + "id": format!("rs_{}", uuid::Uuid::new_v4().to_string().replace('-', "")), + "type": "reasoning", + "summary": [{ + "type": "summary_text", + "text": thinking_text, + }], + }) +} + +/// Build a message output item. +pub fn build_message_output(msg_id: &str, text: &str) -> serde_json::Value { + serde_json::json!({ + "type": "message", + "id": msg_id, + "status": "completed", + "role": "assistant", + "content": [{ + "type": "output_text", + "text": text, + "annotations": [], + }], + }) +} + +/// Build an in-progress message output item (no content yet, for streaming). +pub fn build_message_output_in_progress(msg_id: &str) -> serde_json::Value { + serde_json::json!({ + "type": "message", + "id": msg_id, + "status": "in_progress", + "role": "assistant", + "content": [], + }) +} + // ─── Helpers ───────────────────────────────────────────────────────────────── /// Serialize Option as either the number or JSON null (not omitted).