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:
@@ -72,11 +72,9 @@ struct ResponseData {
|
|||||||
status: &'static str,
|
status: &'static str,
|
||||||
created_at: u64,
|
created_at: u64,
|
||||||
completed_at: Option<u64>,
|
completed_at: Option<u64>,
|
||||||
output: Vec<ResponseOutput>,
|
output: Vec<serde_json::Value>,
|
||||||
usage: Option<Usage>,
|
usage: Option<Usage>,
|
||||||
thinking_signature: Option<String>,
|
thinking_signature: Option<String>,
|
||||||
thinking: Option<String>,
|
|
||||||
thinking_duration: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a full Response object matching the official OpenAI schema.
|
/// 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(),
|
user: params.user.clone(),
|
||||||
metadata: params.metadata.clone(),
|
metadata: params.metadata.clone(),
|
||||||
thinking_signature: data.thinking_signature,
|
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;
|
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<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(
|
let resp = build_response_object(
|
||||||
ResponseData {
|
ResponseData {
|
||||||
id: response_id,
|
id: response_id,
|
||||||
@@ -346,21 +349,9 @@ async fn handle_responses_sync(
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
created_at,
|
created_at,
|
||||||
completed_at: Some(completed_at),
|
completed_at: Some(completed_at),
|
||||||
output: vec![ResponseOutput {
|
output: output_items,
|
||||||
output_type: "message",
|
|
||||||
id: msg_id,
|
|
||||||
status: "completed",
|
|
||||||
role: "assistant",
|
|
||||||
content: vec![OutputContent {
|
|
||||||
content_type: "output_text",
|
|
||||||
text: poll_result.text,
|
|
||||||
annotations: vec![],
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
usage: Some(usage),
|
usage: Some(usage),
|
||||||
thinking_signature: poll_result.thinking_signature,
|
thinking_signature: poll_result.thinking_signature,
|
||||||
thinking: poll_result.thinking,
|
|
||||||
thinking_duration: poll_result.thinking_duration,
|
|
||||||
},
|
},
|
||||||
¶ms,
|
¶ms,
|
||||||
);
|
);
|
||||||
@@ -397,8 +388,6 @@ async fn handle_responses_stream(
|
|||||||
output: vec![],
|
output: vec![],
|
||||||
usage: None,
|
usage: None,
|
||||||
thinking_signature: None,
|
thinking_signature: None,
|
||||||
thinking: None,
|
|
||||||
thinking_duration: None,
|
|
||||||
},
|
},
|
||||||
¶ms,
|
¶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(
|
yield Ok(responses_sse_event(
|
||||||
"response.output_item.added",
|
"response.output_item.added",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"type": "response.output_item.added",
|
"type": "response.output_item.added",
|
||||||
"sequence_number": next_seq(),
|
"sequence_number": next_seq(),
|
||||||
"output_index": OUTPUT_IDX,
|
"output_index": OUTPUT_IDX,
|
||||||
"item": {
|
"item": build_message_output_in_progress(&msg_id),
|
||||||
"type": "message",
|
|
||||||
"id": &msg_id,
|
|
||||||
"status": "in_progress",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [],
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -554,8 +537,6 @@ async fn handle_responses_stream(
|
|||||||
output: vec![],
|
output: vec![],
|
||||||
usage: Some(Usage::estimate(¶ms.user_text, "")),
|
usage: Some(Usage::estimate(¶ms.user_text, "")),
|
||||||
thinking_signature: None,
|
thinking_signature: None,
|
||||||
thinking: None,
|
|
||||||
thinking_duration: None,
|
|
||||||
},
|
},
|
||||||
¶ms,
|
¶ms,
|
||||||
);
|
);
|
||||||
@@ -584,7 +565,7 @@ async fn handle_responses_stream(
|
|||||||
/// 1. response.output_text.done
|
/// 1. response.output_text.done
|
||||||
/// 2. response.content_part.done
|
/// 2. response.content_part.done
|
||||||
/// 3. response.output_item.done
|
/// 3. response.output_item.done
|
||||||
/// 4. response.completed
|
/// 4. response.completed (with reasoning item prepended if present)
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn completion_events(
|
fn completion_events(
|
||||||
resp_id: &str,
|
resp_id: &str,
|
||||||
@@ -599,22 +580,19 @@ fn completion_events(
|
|||||||
params: &RequestParams,
|
params: &RequestParams,
|
||||||
thinking_signature: Option<String>,
|
thinking_signature: Option<String>,
|
||||||
thinking: Option<String>,
|
thinking: Option<String>,
|
||||||
thinking_duration: Option<String>,
|
_thinking_duration: Option<String>,
|
||||||
) -> Vec<Event> {
|
) -> Vec<Event> {
|
||||||
let next_seq = || seq.fetch_add(1, Ordering::Relaxed);
|
let next_seq = || seq.fetch_add(1, Ordering::Relaxed);
|
||||||
let completed_at = now_unix();
|
let completed_at = now_unix();
|
||||||
|
|
||||||
let output_item = serde_json::json!({
|
let output_item = build_message_output(msg_id, text);
|
||||||
"type": "message",
|
|
||||||
"id": msg_id,
|
// Build output array: [reasoning (if present), message]
|
||||||
"status": "completed",
|
let mut output_items: Vec<serde_json::Value> = Vec::new();
|
||||||
"role": "assistant",
|
if let Some(ref thinking_text) = thinking {
|
||||||
"content": [{
|
output_items.push(build_reasoning_output(thinking_text));
|
||||||
"type": "output_text",
|
}
|
||||||
"text": text,
|
output_items.push(build_message_output(msg_id, text));
|
||||||
"annotations": [],
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
let completed_resp = build_response_object(
|
let completed_resp = build_response_object(
|
||||||
ResponseData {
|
ResponseData {
|
||||||
@@ -623,21 +601,9 @@ fn completion_events(
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
created_at,
|
created_at,
|
||||||
completed_at: Some(completed_at),
|
completed_at: Some(completed_at),
|
||||||
output: vec![ResponseOutput {
|
output: output_items,
|
||||||
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![],
|
|
||||||
}],
|
|
||||||
}],
|
|
||||||
usage: Some(usage),
|
usage: Some(usage),
|
||||||
thinking_signature,
|
thinking_signature,
|
||||||
thinking,
|
|
||||||
thinking_duration,
|
|
||||||
},
|
},
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ pub(crate) struct ResponsesResponse {
|
|||||||
#[serde(serialize_with = "serialize_option_u64")]
|
#[serde(serialize_with = "serialize_option_u64")]
|
||||||
pub max_output_tokens: Option<u64>,
|
pub max_output_tokens: Option<u64>,
|
||||||
pub model: String,
|
pub model: String,
|
||||||
pub output: Vec<ResponseOutput>,
|
/// Output items — can contain both `"reasoning"` and `"message"` items.
|
||||||
|
/// Uses serde_json::Value because reasoning and message items have different shapes.
|
||||||
|
pub output: Vec<serde_json::Value>,
|
||||||
pub parallel_tool_calls: bool,
|
pub parallel_tool_calls: bool,
|
||||||
pub previous_response_id: Option<String>,
|
pub previous_response_id: Option<String>,
|
||||||
pub reasoning: Reasoning,
|
pub reasoning: Reasoning,
|
||||||
@@ -91,34 +93,10 @@ pub(crate) struct ResponsesResponse {
|
|||||||
pub user: Option<String>,
|
pub user: Option<String>,
|
||||||
pub metadata: serde_json::Value,
|
pub metadata: serde_json::Value,
|
||||||
/// Proxy extension: opaque thinking verification signature.
|
/// 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")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub thinking_signature: Option<String>,
|
pub thinking_signature: Option<String>,
|
||||||
/// 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<String>,
|
|
||||||
/// Proxy extension: time spent thinking (e.g. "0.041999832s").
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub thinking_duration: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<OutputContent>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
pub(crate) struct OutputContent {
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub content_type: &'static str,
|
|
||||||
pub text: String,
|
|
||||||
pub annotations: Vec<serde_json::Value>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[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 ─────────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Serialize Option<u64> as either the number or JSON null (not omitted).
|
/// Serialize Option<u64> as either the number or JSON null (not omitted).
|
||||||
|
|||||||
Reference in New Issue
Block a user