1mod content;
2mod reasoning;
3use super::common::{Budget, StreamEnd, StreamLimits, declared, invalid, unsupported};
4use crate::{
5 Dialect,
6 transform::generate::stream::{
7 chat::{ChatStreamCollector, ChatStreamLimits},
8 responses::{ResponsesStreamCollector, ResponsesStreamLimits},
9 },
10 transform::{
11 Converted, Report, TransformError,
12 identity::{IdentityFlow, IdentityRole, SourceIdentity, TargetIdPolicy},
13 },
14 wire::openai::{
15 chat::{response as c, stream as cs},
16 responses::{input as i, response as r, stream as rs},
17 },
18};
19use std::collections::BTreeMap;
20
21pub(super) struct TextPart {
22 pub pending: String,
23 pub refusal: bool,
24 pub done: bool,
25 pub emitted: bool,
26}
27
28pub(super) enum ItemKind {
29 Excluded,
30 Message {
31 parts: BTreeMap<i64, TextPart>,
32 cursor: i64,
33 done: bool,
34 },
35 Function {
36 call_id: String,
37 name: String,
38 tool_index: i64,
39 argument_bytes: usize,
40 sent: bool,
41 },
42 Reasoning(reasoning::ReasoningState),
43}
44
45pub(super) struct ItemState {
46 pub id: Option<String>,
47 pub kind: ItemKind,
48}
49
50pub struct ResponsesToChatStream {
53 pub(super) source: Option<ResponsesStreamCollector>,
54 pub(super) target: Option<ChatStreamCollector>,
55 pub(super) flow: IdentityFlow,
56 pub(super) policy: TargetIdPolicy,
57 pub(super) budget: Budget,
58 pub(super) response_id: Option<String>,
59 pub(super) model: Option<String>,
60 pub(super) created: Option<i64>,
61 pub(super) items: BTreeMap<i64, ItemState>,
62 pub(super) content_cursor: i64,
63 pub(super) tools: usize,
64 reasoning_cursor: i64,
65 reasoning_parts: usize,
66 reasoning_text: String,
67 reasoning_details: usize,
68 pub(super) report: Report,
69 pub(super) expected: Option<c::GenerateContentResponseBody>,
70 pub(super) terminal: bool,
71 pub(super) failed: bool,
72}
73
74impl ResponsesToChatStream {
75 pub fn new(flow: IdentityFlow, limits: StreamLimits) -> Self {
76 Self::new_with_policy(
77 flow,
78 limits,
79 TargetIdPolicy::new(crate::Dialect::OpenAiChat),
80 )
81 .expect("default policy matches target dialect")
82 }
83 pub fn new_with_policy(
84 flow: IdentityFlow,
85 limits: StreamLimits,
86 policy: TargetIdPolicy,
87 ) -> Result<Self, TransformError> {
88 if policy.dialect != crate::Dialect::OpenAiChat {
89 return Err(TransformError::shape(
90 "stream.policy",
91 "target policy dialect mismatch",
92 ));
93 }
94
95 let mut collector_policy = policy.clone();
99 collector_policy.preserve_source_ids = true;
100 Ok(Self {
101 source: Some(ResponsesStreamCollector::new(ResponsesStreamLimits {
102 max_bytes: limits.max_bytes,
103
104 max_text_bytes: limits.max_bytes,
105 max_json_bytes: limits.max_bytes,
106 })),
107 target: Some(ChatStreamCollector::with_limits(
108 IdentityFlow::new(flow.namespace()),
109 collector_policy,
110 ChatStreamLimits {
111 max_bytes: limits.max_bytes,
112 },
113 )),
114 flow,
115 policy,
116 budget: Budget::new(limits),
117 response_id: None,
118 model: None,
119 created: None,
120 items: BTreeMap::new(),
121 content_cursor: 0,
122 tools: 0,
123 reasoning_cursor: 0,
124 reasoning_parts: 0,
125 reasoning_text: String::new(),
126 reasoning_details: 0,
127 report: Report::default(),
128 expected: None,
129 terminal: false,
130 failed: false,
131 })
132 }
133 pub(crate) fn reserve_external_ids(
134 &mut self,
135 role: crate::transform::identity::IdentityRole,
136 ids: &std::collections::BTreeSet<String>,
137 ) -> Result<(), TransformError> {
138 self.flow.reserve_external_ids(role, ids).map_err(|e| {
139 TransformError::new(
140 crate::transform::TransformErrorKind::Conflict,
141 "fanout.ids",
142 e.to_string(),
143 )
144 })
145 }
146 pub fn identities(&self) -> &IdentityFlow {
147 &self.flow
148 }
149 pub fn push(
150 &mut self,
151 event: rs::StreamEvent,
152 ) -> Result<Converted<Vec<cs::ChatCompletionChunk>>, TransformError> {
153 if matches!(event, rs::StreamEvent::Unknown) {
154 return Ok(Converted {
155 value: Vec::new(),
156 report: Report::default(),
157 });
158 }
159 if self.failed || self.terminal {
160 self.failed = true;
161 return Err(invalid("event after terminal or failed stream"));
162 }
163 let result = self.push_inner(declared(event));
164 if result.is_err() {
165 self.failed = true;
166 }
167 result
168 }
169 fn push_inner(
170 &mut self,
171 event: rs::StreamEvent,
172 ) -> Result<Converted<Vec<cs::ChatCompletionChunk>>, TransformError> {
173 let event = crate::transform::generate::multi_agent::attribute_event(event)?;
174 self.budget.input(&event)?;
175 self.source
176 .as_mut()
177 .ok_or_else(|| invalid("source consumed"))?
178 .push(event.clone())?;
179 let mut out = Vec::new();
180 match event {
181 rs::StreamEvent::Keepalive
182 | rs::StreamEvent::Metadata(_)
183 | rs::StreamEvent::Unknown => {}
184 rs::StreamEvent::Created(v) => {
185 let id = self
186 .flow
187 .resolve_or_allocate(
188 IdentityRole::Response,
189 SourceIdentity::new(Dialect::OpenAi, Some(v.response.id), 0),
190 &self.policy,
191 )
192 .map_err(|e| {
193 TransformError::invalid_result("identity.response", e.to_string())
194 })?
195 .emitted_id;
196 self.response_id = Some(id);
197 self.model = Some(v.response.model);
198 self.created = Some(v.response.created_at);
199 self.emit_delta(
200 cs::Delta::builder().role(cs::DeltaRole::Assistant).build(),
201 &mut out,
202 )?;
203 for (index, item) in v.response.output.into_iter().enumerate() {
204 self.add_item(index as i64, item, &mut out)?;
205 }
206 }
207 rs::StreamEvent::OutputItemAdded(v) => {
208 self.add_item(v.output_index, v.item, &mut out)?
209 }
210 rs::StreamEvent::ContentPartAdded(v) => {
211 self.add_part(v.output_index, v.content_index, v.part)?
212 }
213 rs::StreamEvent::OutputTextDelta(v) => {
214 self.append_text(v.output_index, v.content_index, &v.item_id, v.delta, false)?
215 }
216 rs::StreamEvent::RefusalDelta(v) => {
217 self.append_text(v.output_index, v.content_index, &v.item_id, v.delta, true)?
218 }
219 rs::StreamEvent::ContentPartDone(v) => {
220 if let Some(ItemState {
221 kind: ItemKind::Message { parts, .. },
222 ..
223 }) = self.items.get_mut(&v.output_index)
224 {
225 parts
226 .get_mut(&v.content_index)
227 .ok_or_else(|| invalid("part done without part"))?
228 .done = true;
229 }
230 }
231 rs::StreamEvent::OutputItemDone(v) => {
232 self.bind_item(v.output_index, item_id(&v.item))?;
233 if let r::ResponseOutputItem::Reasoning(item) = &v.item
234 && let Some(state) = self.reasoning_mut(v.output_index, &item.id)?
235 {
236 for part in state.summary.values_mut() {
237 part.done = true;
238 }
239 state.final_item = Some(item.clone());
240 }
241 if let Some(ItemState {
242 kind: ItemKind::Message { parts, done, .. },
243 ..
244 }) = self.items.get_mut(&v.output_index)
245 {
246 *done = true;
247 for p in parts.values_mut() {
248 p.done = true;
249 }
250 }
251 }
252 rs::StreamEvent::FunctionCallArgumentsDelta(v) => {
253 self.bind_item(v.output_index, Some(&v.item_id))?;
254 self.emit_tool(v.output_index, v.delta, &mut out)?;
255 }
256 rs::StreamEvent::FunctionCallArgumentsDone(v) => {
257 self.bind_item(v.output_index, Some(&v.item_id))?;
258 if let Some(ItemState {
259 kind: ItemKind::Function { argument_bytes, .. },
260 ..
261 }) = self.items.get(&v.output_index)
262 {
263 let suffix = v.arguments[*argument_bytes..].to_owned();
264 if !suffix.is_empty() {
265 self.emit_tool(v.output_index, suffix, &mut out)?;
266 }
267 }
268 }
269 rs::StreamEvent::Completed(_) | rs::StreamEvent::Incomplete(_) => {
270 self.finish_source(&mut out)?;
271 }
272 rs::StreamEvent::Queued(_)
273 | rs::StreamEvent::InProgress(_)
274 | rs::StreamEvent::OutputTextDone(_)
275 | rs::StreamEvent::RefusalDone(_) => {}
276 rs::StreamEvent::OutputTextAnnotationAdded(_) => self.report.omitted(
277 "response.annotations",
278 "Chat stream Delta has no annotation field",
279 ),
280 rs::StreamEvent::ReasoningSummaryPartAdded(v) => {
281 if let Some(state) = self.reasoning_mut(v.output_index, &v.item_id)? {
282 state.summary.insert(
283 v.summary_index,
284 TextPart {
285 pending: v.part.text,
286 refusal: false,
287 done: false,
288 emitted: false,
289 },
290 );
291 }
292 }
293 rs::StreamEvent::ReasoningSummaryTextDelta(v) => {
294 if let Some(state) = self.reasoning_mut(v.output_index, &v.item_id)? {
295 state
296 .summary
297 .get_mut(&v.summary_index)
298 .ok_or_else(|| invalid("reasoning delta without summary part"))?
299 .pending
300 .push_str(&v.delta);
301 }
302 }
303 rs::StreamEvent::ReasoningSummaryPartDone(v) => {
304 if let Some(state) = self.reasoning_mut(v.output_index, &v.item_id)? {
305 state
306 .summary
307 .get_mut(&v.summary_index)
308 .ok_or_else(|| invalid("reasoning done without summary part"))?
309 .done = true;
310 }
311 }
312 rs::StreamEvent::ReasoningTextDelta(_)
315 | rs::StreamEvent::ReasoningTextDone(_)
316 | rs::StreamEvent::ReasoningSummaryTextDone(_) => {}
317 rs::StreamEvent::Failed(_) | rs::StreamEvent::Error(_) => {
318 return Err(invalid("Responses stream failed"));
319 }
320 rs::StreamEvent::CustomToolInputDelta(_) | rs::StreamEvent::CustomToolInputDone(_) => {
321 return Err(unsupported(
322 "custom_tool",
323 "Chat streaming tool schema supports only function calls; buffered Chat has a separate custom-tool shape",
324 ));
325 }
326 rs::StreamEvent::AudioDelta(_)
327 | rs::StreamEvent::AudioDone(_)
328 | rs::StreamEvent::AudioTranscriptDelta(_)
329 | rs::StreamEvent::AudioTranscriptDone(_) => {
330 return Err(unsupported(
331 "audio",
332 "this native stream wire has no canonical output audio item",
333 ));
334 }
335 _ => {
336 return Err(unsupported(
337 "response.execution",
338 "native execution events require an invocation adapter",
339 ));
340 }
341 }
342 if !self.terminal {
343 self.flush_text(&mut out)?;
344 self.flush_reasoning(&mut out)?;
345 }
346 Ok(Converted {
347 value: out,
348 report: std::mem::take(&mut self.report),
349 })
350 }
351 fn finish_source(
352 &mut self,
353 out: &mut Vec<cs::ChatCompletionChunk>,
354 ) -> Result<(), TransformError> {
355 let actual = self
356 .source
357 .take()
358 .ok_or_else(|| invalid("source consumed"))?
359 .finish()?;
360 self.report.diagnostics.extend(actual.report.diagnostics);
361 let mapped =
362 super::super::responses_to_chat_response(actual.value, &mut self.flow, &self.policy)?;
363 self.report.diagnostics.extend(mapped.report.diagnostics);
364 for item in self.items.values_mut() {
365 if let ItemKind::Message { parts, done, .. } = &mut item.kind {
366 *done = true;
367 for p in parts.values_mut() {
368 p.done = true;
369 }
370 }
371 }
372 self.flush_text(out)?;
373 let mut expected = mapped.value;
374 let message = &expected.choices[0].message;
375 if message.reasoning_details.is_some() || message.reasoning_content.is_some() {
376 let mut delta = cs::Delta::builder().build();
377 if let Some(Some(details)) = &message.reasoning_details {
378 let remaining = details
379 .get(self.reasoning_details..)
380 .ok_or_else(|| invalid("reasoning details exceed final result"))?;
381 if !remaining.is_empty() {
382 delta.reasoning_details = Some(Some(remaining.to_vec()));
383 }
384 }
385 if let Some(Some(text)) = &message.reasoning_content {
386 let suffix = text
387 .strip_prefix(&self.reasoning_text)
388 .ok_or_else(|| invalid("streamed reasoning differs from final result"))?;
389 if !suffix.is_empty() || self.reasoning_parts == 0 {
390 delta.reasoning_content = Some(Some(suffix.to_owned()));
391 }
392 }
393 if delta.reasoning_details.is_some() || delta.reasoning_content.is_some() {
394 self.emit_delta(delta, out)?;
395 }
396 }
397 if expected.choices[0].message.annotations.take().is_some() {
398 self.report.omitted(
399 "message.annotations",
400 "Chat streaming Delta has no annotation field",
401 );
402 }
403 let logs = expected.choices[0]
404 .logprobs
405 .clone()
406 .map(crate::transform::generate::stream::chat::logs::synthesize)
407 .map(crate::transform::optional)
408 .transpose()?
409 .flatten();
410 let mut choice = cs::StreamChoice::builder(0, cs::Delta::builder().build())
411 .finish_reason(expected.choices[0].finish_reason)
412 .build();
413 choice.logprobs = logs.map(Some);
414 let mut terminal = self.chunk(vec![choice])?;
415 terminal.service_tier = expected.service_tier;
416 terminal.moderation = expected.moderation.clone();
417 self.emit(terminal, out)?;
418 if let Some(usage) = expected.usage.clone() {
419 let usage = crate::transform::generate::stream::chat::usage::synthesize(usage)?;
420 let mut chunk = self.chunk(Vec::new())?;
421 chunk.usage = Some(Some(usage));
422 self.emit(chunk, out)?;
423 }
424 self.expected = Some(expected);
425 self.terminal = true;
426 Ok(())
427 }
428 fn emit_delta(
429 &mut self,
430 delta: cs::Delta,
431 out: &mut Vec<cs::ChatCompletionChunk>,
432 ) -> Result<(), TransformError> {
433 let chunk = self.chunk(vec![cs::StreamChoice::builder(0, delta).build()])?;
434 self.emit(chunk, out)
435 }
436 fn chunk(
437 &self,
438 choices: Vec<cs::StreamChoice>,
439 ) -> Result<cs::ChatCompletionChunk, TransformError> {
440 Ok(cs::ChatCompletionChunk::builder(
441 self.response_id
442 .clone()
443 .ok_or_else(|| TransformError::missing_metadata("response.id"))?,
444 choices,
445 self.created
446 .ok_or_else(|| TransformError::missing_metadata("response.created_at"))?,
447 self.model
448 .clone()
449 .ok_or_else(|| TransformError::missing_metadata("response.model"))?,
450 cs::ChunkObject::ChatCompletionChunk,
451 )
452 .build())
453 }
454 fn emit(
455 &mut self,
456 chunk: cs::ChatCompletionChunk,
457 out: &mut Vec<cs::ChatCompletionChunk>,
458 ) -> Result<(), TransformError> {
459 self.budget.output(&chunk)?;
460 self.target
461 .as_mut()
462 .ok_or_else(|| invalid("target consumed"))?
463 .push(chunk.clone())?;
464 out.push(chunk);
465 Ok(())
466 }
467 pub fn finish(mut self) -> Result<StreamEnd<cs::ChatCompletionChunk>, TransformError> {
468 if self.failed || !self.terminal {
469 return Err(invalid("Responses EOF without terminal"));
470 }
471 let mut target = self
472 .target
473 .take()
474 .ok_or_else(|| invalid("target consumed"))?;
475 target.push_done()?;
476 let actual = target.finish()?;
477 if Some(&actual.value) != self.expected.as_ref() {
478 return Err(invalid(
479 "emitted Chat content/identity/metadata differs from native terminal response",
480 ));
481 }
482 self.report.diagnostics.extend(actual.report.diagnostics);
483 Ok(StreamEnd {
484 chunks: Vec::new(),
485 identities: self.flow,
486 report: self.report,
487 })
488 }
489}
490
491fn item_id(item: &r::ResponseOutputItem) -> Option<&str> {
492 match item {
493 r::ResponseOutputItem::Message(v) => Some(&v.id),
494 r::ResponseOutputItem::FunctionCall(v) => v.id.as_deref(),
495 r::ResponseOutputItem::Reasoning(v) => Some(&v.id),
496 _ => None,
497 }
498}