|
3 | 3 |
|
4 | 4 | //! Streaming codec for OpenAI Responses API events. |
5 | 5 |
|
| 6 | +use serde::Serialize; |
6 | 7 | use serde_json::{Value, json}; |
7 | 8 |
|
8 | 9 | use crate::LlmResponseChunk; |
@@ -34,14 +35,63 @@ impl StreamCodec for OpenAiResponsesStreamCodec { |
34 | 35 | state: &mut StreamTranslationState, |
35 | 36 | event: LlmResponseChunk, |
36 | 37 | ) -> Vec<Value> { |
37 | | - encode_responses_stream(state, event) |
| 38 | + let events = encode_responses_stream(state, event); |
| 39 | + add_sequence_numbers(state, events) |
| 40 | + } |
| 41 | + |
| 42 | + fn observe_replayed_event( |
| 43 | + &self, |
| 44 | + state: &mut StreamTranslationState, |
| 45 | + raw: &Value, |
| 46 | + normalized: Vec<LlmResponseChunk>, |
| 47 | + ) { |
| 48 | + let replayed_terminal = normalized |
| 49 | + .iter() |
| 50 | + .any(|chunk| matches!(chunk, LlmResponseChunk::MessageStop { .. })); |
| 51 | + for chunk in normalized { |
| 52 | + drop(encode_responses_stream(state, chunk)); |
| 53 | + } |
| 54 | + state.response_sequence_number = raw |
| 55 | + .get("sequence_number") |
| 56 | + .and_then(Value::as_u64) |
| 57 | + .map_or(state.response_sequence_number.saturating_add(1), |number| { |
| 58 | + number.saturating_add(1) |
| 59 | + }); |
| 60 | + if replayed_terminal { |
| 61 | + state.finished = true; |
| 62 | + } |
38 | 63 | } |
39 | 64 |
|
40 | 65 | fn finish(&self, state: &mut StreamTranslationState) -> Vec<Value> { |
41 | | - finish_responses_stream(state) |
| 66 | + let events = finish_responses_stream(state); |
| 67 | + add_sequence_numbers(state, events) |
42 | 68 | } |
43 | 69 | } |
44 | 70 |
|
| 71 | +/// Required fields shared by Responses stream snapshots. |
| 72 | +#[derive(Serialize)] |
| 73 | +struct ResponsesStreamResponse { |
| 74 | + id: String, |
| 75 | + object: &'static str, |
| 76 | + created_at: u64, |
| 77 | + completed_at: Option<u64>, |
| 78 | + error: Option<Value>, |
| 79 | + incomplete_details: Option<Value>, |
| 80 | + instructions: Option<Value>, |
| 81 | + metadata: Option<Value>, |
| 82 | + model: String, |
| 83 | + output: Vec<Value>, |
| 84 | + parallel_tool_calls: bool, |
| 85 | + frequency_penalty: Option<f64>, |
| 86 | + presence_penalty: Option<f64>, |
| 87 | + status: &'static str, |
| 88 | + temperature: Option<f64>, |
| 89 | + tool_choice: &'static str, |
| 90 | + tools: Vec<Value>, |
| 91 | + top_p: Option<f64>, |
| 92 | + usage: Value, |
| 93 | +} |
| 94 | + |
45 | 95 | // Decodes one OpenAI Responses event into neutral streaming events. |
46 | 96 | fn decode_responses_stream( |
47 | 97 | state: &mut StreamTranslationState, |
@@ -224,6 +274,7 @@ fn finish_responses_stream(state: &mut StreamTranslationState) -> Vec<Value> { |
224 | 274 | "output_index": output_index, |
225 | 275 | "item": { |
226 | 276 | "type": "message", |
| 277 | + "id": format!("msg_{output_index}"), |
227 | 278 | "role": "assistant", |
228 | 279 | "status": status, |
229 | 280 | "content": [{"type": "output_text", "text": state.response_text}], |
@@ -265,6 +316,7 @@ fn finish_responses_stream(state: &mut StreamTranslationState) -> Vec<Value> { |
265 | 316 | output_index, |
266 | 317 | json!({ |
267 | 318 | "type": "message", |
| 319 | + "id": format!("msg_{output_index}"), |
268 | 320 | "role": "assistant", |
269 | 321 | "status": status, |
270 | 322 | "content": [{"type": "output_text", "text": state.response_text}], |
@@ -306,15 +358,7 @@ fn finish_responses_stream(state: &mut StreamTranslationState) -> Vec<Value> { |
306 | 358 |
|
307 | 359 | out.push(json!({ |
308 | 360 | "type": event_type, |
309 | | - "response": { |
310 | | - "id": responses_id(state), |
311 | | - "object": "response", |
312 | | - "status": status, |
313 | | - "incomplete_details": incomplete_details, |
314 | | - "model": target_model_or_source_model(state), |
315 | | - "output": output, |
316 | | - "usage": responses_usage_value(&state.usage), |
317 | | - }, |
| 361 | + "response": responses_stream_response(state, status, incomplete_details, output), |
318 | 362 | })); |
319 | 363 | state.finished = true; |
320 | 364 | out |
@@ -393,17 +437,54 @@ fn ensure_responses_created(state: &mut StreamTranslationState) -> Vec<Value> { |
393 | 437 | state.response_created = true; |
394 | 438 | vec![json!({ |
395 | 439 | "type": "response.created", |
396 | | - "response": { |
397 | | - "id": responses_id(state), |
398 | | - "object": "response", |
399 | | - "status": "in_progress", |
400 | | - "model": target_model_or_source_model(state), |
401 | | - "output": [], |
402 | | - "usage": responses_usage_value(&state.usage), |
403 | | - }, |
| 440 | + "response": responses_stream_response(state, "in_progress", None, Vec::new()), |
404 | 441 | })] |
405 | 442 | } |
406 | 443 |
|
| 444 | +// Builds a schema-complete Responses snapshot for strict generated clients. |
| 445 | +fn responses_stream_response( |
| 446 | + state: &StreamTranslationState, |
| 447 | + status: &'static str, |
| 448 | + incomplete_details: Option<Value>, |
| 449 | + output: Vec<Value>, |
| 450 | +) -> ResponsesStreamResponse { |
| 451 | + ResponsesStreamResponse { |
| 452 | + id: responses_id(state), |
| 453 | + object: "response", |
| 454 | + created_at: 0, |
| 455 | + completed_at: None, |
| 456 | + error: None, |
| 457 | + incomplete_details, |
| 458 | + instructions: None, |
| 459 | + metadata: None, |
| 460 | + model: target_model_or_source_model(state), |
| 461 | + output, |
| 462 | + parallel_tool_calls: true, |
| 463 | + frequency_penalty: None, |
| 464 | + presence_penalty: None, |
| 465 | + status, |
| 466 | + temperature: None, |
| 467 | + tool_choice: "auto", |
| 468 | + tools: Vec::new(), |
| 469 | + top_p: None, |
| 470 | + usage: responses_usage_value(&state.usage), |
| 471 | + } |
| 472 | +} |
| 473 | + |
| 474 | +// Assigns monotonically increasing sequence numbers to generated Responses events. |
| 475 | +fn add_sequence_numbers(state: &mut StreamTranslationState, mut events: Vec<Value>) -> Vec<Value> { |
| 476 | + for event in &mut events { |
| 477 | + if let Some(object) = event.as_object_mut() { |
| 478 | + object.insert( |
| 479 | + "sequence_number".to_string(), |
| 480 | + Value::from(state.response_sequence_number), |
| 481 | + ); |
| 482 | + state.response_sequence_number = state.response_sequence_number.saturating_add(1); |
| 483 | + } |
| 484 | + } |
| 485 | + events |
| 486 | +} |
| 487 | + |
407 | 488 | // Accumulates assistant text and emits Responses text delta events. |
408 | 489 | fn encode_responses_text_delta(state: &mut StreamTranslationState, text: String) -> Vec<Value> { |
409 | 490 | let mut out = ensure_responses_created(state); |
|
0 commit comments