diff --git a/crates/cortex-core/src/responses.rs b/crates/cortex-core/src/responses.rs index 588c9e8..9e94e86 100644 --- a/crates/cortex-core/src/responses.rs +++ b/crates/cortex-core/src/responses.rs @@ -212,6 +212,11 @@ pub struct ResponsesUsage { /// Responses API. pub mod events { pub const CREATED: &str = "response.created"; + /// Fired between `response.created` and the first output-item + /// event. Marks "request validated, model is generating" — + /// some clients use it to differentiate the "warming up" state + /// from "streaming tokens" in their UI. + pub const IN_PROGRESS: &str = "response.in_progress"; pub const OUTPUT_ITEM_ADDED: &str = "response.output_item.added"; pub const CONTENT_PART_ADDED: &str = "response.content_part.added"; pub const OUTPUT_TEXT_DELTA: &str = "response.output_text.delta"; diff --git a/crates/neuron/src/wire/openai_responses.rs b/crates/neuron/src/wire/openai_responses.rs index e7ade8f..8821910 100644 --- a/crates/neuron/src/wire/openai_responses.rs +++ b/crates/neuron/src/wire/openai_responses.rs @@ -325,6 +325,19 @@ async fn emit_start_frames(tx: &mpsc::Sender, meta: &Respon let frames = [ ResponseStreamFrame { event_name: events::CREATED, + data: json!({ "response": shell.clone() }), + }, + // `response.in_progress` carries the same shell as + // `response.created` — both report the "in_progress" + // status and both are payload-light bookkeeping events. + // The distinction is meaningful to clients that + // differentiate "request validated" from "model is + // generating" in their UI (loading spinner vs streaming + // spinner). OpenAI's own Responses SSE emits them as a + // pair; matching the wire shape avoids subtle client + // breakage. + ResponseStreamFrame { + event_name: events::IN_PROGRESS, data: json!({ "response": shell }), }, ResponseStreamFrame { @@ -722,6 +735,7 @@ mod tests { names, vec![ events::CREATED, + events::IN_PROGRESS, events::OUTPUT_ITEM_ADDED, events::CONTENT_PART_ADDED, events::OUTPUT_TEXT_DELTA, @@ -733,15 +747,17 @@ mod tests { ] ); - // The two deltas should carry the right text. - assert_eq!(frames[3].data["delta"], "hel"); - assert_eq!(frames[4].data["delta"], "lo"); + // The two deltas should carry the right text. Indices + // shifted by one after IN_PROGRESS inserted between + // CREATED and OUTPUT_ITEM_ADDED. + assert_eq!(frames[4].data["delta"], "hel"); + assert_eq!(frames[5].data["delta"], "lo"); // The done event has the full accumulated text. - assert_eq!(frames[5].data["text"], "hello"); + assert_eq!(frames[6].data["text"], "hello"); // Completed event carries the full message item. - let completed = &frames[8].data["response"]; + let completed = &frames[9].data["response"]; assert_eq!(completed["status"], "completed"); let output = completed["output"].as_array().unwrap(); assert_eq!(output.len(), 1);