Refactor ResponseItem to typed variant payload structs

This commit is contained in:
Ahmed Ibrahim
2026-02-08 18:18:51 -08:00
parent 45b7763c3f
commit 2e267dec61
45 changed files with 1161 additions and 899 deletions

View File

@@ -36,12 +36,15 @@ impl Stream for AggregatedStream {
Poll::Ready(Some(Ok(ResponseEvent::OutputItemDone(item)))) => {
let is_assistant_message = matches!(
&item,
ResponseItem::Message { role, .. } if role == "assistant"
ResponseItem::Message(codex_protocol::models::Message { role, .. }) if role == "assistant"
);
if is_assistant_message {
if this.cumulative.is_empty()
&& let ResponseItem::Message { content, .. } = &item
&& let ResponseItem::Message(codex_protocol::models::Message {
content,
..
}) = &item
&& let Some(text) = content.iter().find_map(|c| match c {
ContentItem::OutputText { text } => Some(text),
_ => None,
@@ -70,29 +73,31 @@ impl Stream for AggregatedStream {
let mut emitted_any = false;
if !this.cumulative_reasoning.is_empty() {
let aggregated_reasoning = ResponseItem::Reasoning {
id: String::new(),
summary: Vec::new(),
content: Some(vec![ReasoningItemContent::ReasoningText {
text: std::mem::take(&mut this.cumulative_reasoning),
}]),
encrypted_content: None,
};
let aggregated_reasoning =
ResponseItem::Reasoning(codex_protocol::models::Reasoning {
id: String::new(),
summary: Vec::new(),
content: Some(vec![ReasoningItemContent::ReasoningText {
text: std::mem::take(&mut this.cumulative_reasoning),
}]),
encrypted_content: None,
});
this.pending
.push_back(ResponseEvent::OutputItemDone(aggregated_reasoning));
emitted_any = true;
}
if !this.cumulative.is_empty() {
let aggregated_message = ResponseItem::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: std::mem::take(&mut this.cumulative),
}],
end_turn: None,
phase: None,
};
let aggregated_message =
ResponseItem::Message(codex_protocol::models::Message {
id: None,
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: std::mem::take(&mut this.cumulative),
}],
end_turn: None,
phase: None,
});
this.pending
.push_back(ResponseEvent::OutputItemDone(aggregated_message));
emitted_any = true;

View File

@@ -169,12 +169,24 @@ fn attach_item_ids(payload_json: &mut Value, original_items: &[ResponseItem]) {
};
for (value, item) in items.iter_mut().zip(original_items.iter()) {
if let ResponseItem::Reasoning { id, .. }
| ResponseItem::Message { id: Some(id), .. }
| ResponseItem::WebSearchCall { id: Some(id), .. }
| ResponseItem::FunctionCall { id: Some(id), .. }
| ResponseItem::LocalShellCall { id: Some(id), .. }
| ResponseItem::CustomToolCall { id: Some(id), .. } = item
if let ResponseItem::Reasoning(codex_protocol::models::Reasoning { id, .. })
| ResponseItem::Message(codex_protocol::models::Message { id: Some(id), .. })
| ResponseItem::WebSearchCall(codex_protocol::models::WebSearchCall {
id: Some(id),
..
})
| ResponseItem::FunctionCall(codex_protocol::models::FunctionCall {
id: Some(id),
..
})
| ResponseItem::LocalShellCall(codex_protocol::models::LocalShellCall {
id: Some(id),
..
})
| ResponseItem::CustomToolCall(codex_protocol::models::CustomToolCall {
id: Some(id),
..
}) = item
{
if id.is_empty() {
continue;
@@ -217,20 +229,20 @@ mod tests {
fn azure_default_store_attaches_ids_and_headers() {
let provider = provider("azure", "https://example.openai.azure.com/v1");
let input = vec![
ResponseItem::Message {
ResponseItem::Message(codex_protocol::models::Message {
id: Some("m1".into()),
role: "assistant".into(),
content: Vec::new(),
end_turn: None,
phase: None,
},
ResponseItem::Message {
}),
ResponseItem::Message(codex_protocol::models::Message {
id: None,
role: "assistant".into(),
content: Vec::new(),
end_turn: None,
phase: None,
},
}),
];
let request = ResponsesRequestBuilder::new("gpt-test", "inst", &input)

View File

@@ -531,16 +531,16 @@ mod tests {
assert_matches!(
&events[0],
Ok(ResponseEvent::OutputItemDone(ResponseItem::Message {
Ok(ResponseEvent::OutputItemDone(ResponseItem::Message(codex_protocol::models::Message {
role,
phase: Some(MessagePhase::Commentary),
..
})) if role == "assistant"
}))) if role == "assistant"
);
assert_matches!(
&events[1],
Ok(ResponseEvent::OutputItemDone(ResponseItem::Message { role, .. }))
Ok(ResponseEvent::OutputItemDone(ResponseItem::Message(codex_protocol::models::Message { role, .. })))
if role == "assistant"
);

View File

@@ -255,7 +255,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
let prompt = codex_api::Prompt {
instructions: "Say hi".to_string(),
input: vec![ResponseItem::Message {
input: vec![ResponseItem::Message(codex_protocol::models::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
@@ -263,7 +263,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> {
}],
end_turn: None,
phase: None,
}],
})],
tools: Vec::<Value>::new(),
parallel_tool_calls: false,
output_schema: None,

View File

@@ -144,14 +144,20 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()>
assert_eq!(events.len(), 3);
match &events[0] {
ResponseEvent::OutputItemDone(ResponseItem::Message { role, .. }) => {
ResponseEvent::OutputItemDone(ResponseItem::Message(codex_protocol::models::Message {
role,
..
})) => {
assert_eq!(role, "assistant");
}
other => panic!("unexpected first event: {other:?}"),
}
match &events[1] {
ResponseEvent::OutputItemDone(ResponseItem::Message { role, .. }) => {
ResponseEvent::OutputItemDone(ResponseItem::Message(codex_protocol::models::Message {
role,
..
})) => {
assert_eq!(role, "assistant");
}
other => panic!("unexpected second event: {other:?}"),
@@ -215,7 +221,10 @@ async fn responses_stream_aggregates_output_text_deltas() -> Result<()> {
assert_eq!(events.len(), 2);
match &events[0] {
ResponseEvent::OutputItemDone(ResponseItem::Message { content, .. }) => {
ResponseEvent::OutputItemDone(ResponseItem::Message(codex_protocol::models::Message {
content,
..
})) => {
let mut aggregated = String::new();
for item in content {
if let ContentItem::OutputText { text } = item {