1use crate::llm::error_display;
2use crate::llm::provider::{
3 AssistantPhase, ContentPart, LLMError, LLMResponse, LLMStreamEvent, Message, MessageContent,
4 MessageRole, ToolCall,
5};
6pub use crate::llm::providers::ReasoningBuffer;
7use crate::llm::providers::common::{
8 extract_reasoning_text_from_serialized_details, map_finish_reason_common,
9};
10mod responses_stream;
11mod tag_sanitizer;
12use crate::llm::providers::split_reasoning_from_text;
13pub use responses_stream::{ResponsesNormalizedStreamOptions, create_responses_normalized_stream};
14use serde_json::{Map, Value};
15pub use tag_sanitizer::TagStreamSanitizer;
16
17pub(crate) fn parse_cached_prompt_tokens_from_usage(
18 usage_value: &Value,
19 include_cached_prompt_metrics: bool,
20) -> Option<u32> {
21 if !include_cached_prompt_metrics {
22 return None;
23 }
24
25 let cached_prompt_tokens = usage_value
26 .get("input_tokens_details")
27 .and_then(|details| details.get("cached_tokens"))
28 .or_else(|| {
29 usage_value
30 .get("prompt_tokens_details")
31 .and_then(|details| details.get("cached_tokens"))
32 })
33 .or_else(|| usage_value.get("prompt_cache_hit_tokens"))
34 .or_else(|| usage_value.get("cached_tokens"))
35 .and_then(Value::as_u64)
36 .and_then(|value| u32::try_from(value).ok());
37
38 if let Some(cached_prompt_tokens) = cached_prompt_tokens {
39 tracing::debug!(
40 target = "vtcode::llm::responses::prompt_cache",
41 cached_prompt_tokens,
42 "Responses cached prompt token usage"
43 );
44 }
45
46 cached_prompt_tokens
47}
48
49#[derive(Debug, thiserror::Error)]
50pub enum StreamAssemblyError {
51 #[error("missing field `{0}` in stream payload")]
52 MissingField(&'static str),
53 #[error("invalid stream payload: {0}")]
54 InvalidPayload(String),
55}
56
57impl StreamAssemblyError {
58 #[cold]
59 pub fn into_llm_error(self, provider: &str) -> LLMError {
60 let message = self.to_string();
61 let formatted = error_display::format_llm_error(provider, &message);
62 LLMError::Provider {
63 message: formatted,
64 metadata: None,
65 }
66 }
67}
68
69pub trait StreamTelemetry: Send + Sync {
70 fn on_content_delta(&self, _delta: &str) {}
71 fn on_reasoning_delta(&self, _delta: &str) {}
72 fn on_reasoning_stage(&self, _stage: &str) {}
73 fn on_tool_call_delta(&self) {}
74}
75
76#[derive(Default)]
77pub struct NoopStreamTelemetry;
78
79impl StreamTelemetry for NoopStreamTelemetry {}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum StreamFragment {
83 Content(String),
84 Reasoning(String),
85}
86
87#[derive(Default, Debug)]
88pub struct StreamDelta {
89 fragments: Vec<StreamFragment>,
90}
91
92impl StreamDelta {
93 pub fn push_content(&mut self, text: &str) {
94 if text.is_empty() {
95 return;
96 }
97
98 match self.fragments.last_mut() {
99 Some(StreamFragment::Content(existing)) => existing.push_str(text),
100 _ => self
101 .fragments
102 .push(StreamFragment::Content(text.to_string())),
103 }
104 }
105
106 pub fn push_reasoning(&mut self, text: &str) {
107 if text.is_empty() {
108 return;
109 }
110
111 match self.fragments.last_mut() {
112 Some(StreamFragment::Reasoning(existing)) => existing.push_str(text),
113 _ => self
114 .fragments
115 .push(StreamFragment::Reasoning(text.to_string())),
116 }
117 }
118
119 pub fn is_empty(&self) -> bool {
120 self.fragments.is_empty()
121 }
122
123 pub fn into_fragments(self) -> Vec<StreamFragment> {
124 self.fragments
125 }
126
127 pub fn extend(&mut self, other: StreamDelta) {
128 self.fragments.extend(other.fragments);
129 }
130}
131
132#[derive(Default, Clone)]
133pub struct ToolCallBuilder {
134 id: Option<String>,
135 namespace: Option<String>,
136 name: Option<String>,
137 arguments: String,
138}
139
140impl ToolCallBuilder {
141 pub fn apply_delta(&mut self, delta: &Value) {
142 if let Some(id) = delta.get("id").and_then(|value| value.as_str()) {
143 self.id = Some(id.to_string());
144 }
145
146 if let Some(namespace) = delta.get("namespace").and_then(|value| value.as_str()) {
147 self.namespace = Some(namespace.to_string());
148 }
149
150 if let Some(function) = delta.get("function") {
151 if let Some(namespace) = function.get("namespace").and_then(|value| value.as_str()) {
152 self.namespace = Some(namespace.to_string());
153 }
154
155 if let Some(name) = function.get("name").and_then(|value| value.as_str()) {
156 self.name = Some(name.to_string());
157 }
158
159 if let Some(arguments_value) = function.get("arguments") {
160 if let Some(arguments) = arguments_value.as_str() {
161 self.arguments.push_str(arguments);
162 } else if arguments_value.is_object() || arguments_value.is_array() {
163 self.arguments.push_str(&arguments_value.to_string());
164 }
165 }
166 }
167 }
168
169 pub fn finalize(self, fallback_index: usize) -> Option<ToolCall> {
170 let name = self.name?;
171 let id = self
172 .id
173 .unwrap_or_else(|| format!("tool_call_{fallback_index}"));
174 let arguments = if self.arguments.is_empty() {
175 "{}".to_string()
176 } else {
177 self.arguments
178 };
179
180 Some(ToolCall::function_with_namespace(
181 id,
182 self.namespace,
183 name,
184 arguments,
185 ))
186 }
187}
188
189pub fn update_tool_calls(builders: &mut Vec<ToolCallBuilder>, deltas: &[Value]) {
190 for (position, delta) in deltas.iter().enumerate() {
191 let index = delta
192 .get("index")
193 .and_then(|value| value.as_u64())
194 .map(|value| value as usize)
195 .unwrap_or(position);
196
197 if builders.len() <= index {
198 builders.resize_with(index + 1, ToolCallBuilder::default);
199 }
200 let Some(builder) = builders.get_mut(index) else {
201 continue;
202 };
203
204 builder.apply_delta(delta);
205 }
206}
207
208pub fn finalize_tool_calls(builders: Vec<ToolCallBuilder>) -> Option<Vec<ToolCall>> {
209 let calls: Vec<ToolCall> = builders
210 .into_iter()
211 .enumerate()
212 .filter_map(|(index, builder)| builder.finalize(index))
213 .collect();
214
215 (!calls.is_empty()).then_some(calls)
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub(crate) enum FunctionOutputContentItem {
220 InputText { text: String },
221 InputImage { image_url: String },
222}
223
224impl FunctionOutputContentItem {
225 fn from_value(value: &Value) -> Option<Self> {
226 let item_type = value.get("type").and_then(Value::as_str)?;
227 match item_type {
228 "input_text" | "output_text" => Some(Self::InputText {
229 text: value.get("text").and_then(Value::as_str)?.to_string(),
230 }),
231 "input_image" => Some(Self::InputImage {
232 image_url: value.get("image_url").and_then(Value::as_str)?.to_string(),
233 }),
234 _ => None,
235 }
236 }
237
238 pub(crate) fn to_function_output_json(&self) -> Value {
239 match self {
240 Self::InputText { text } => serde_json::json!({
241 "type": "input_text",
242 "text": text
243 }),
244 Self::InputImage { image_url } => serde_json::json!({
245 "type": "input_image",
246 "image_url": image_url
247 }),
248 }
249 }
250
251 pub(crate) fn to_tool_result_json(&self) -> Value {
252 match self {
253 Self::InputText { text } => serde_json::json!({
254 "type": "output_text",
255 "text": text
256 }),
257 Self::InputImage { image_url } => serde_json::json!({
258 "type": "input_image",
259 "image_url": image_url
260 }),
261 }
262 }
263}
264
265fn parse_function_output_content_items_array(
266 items: &[Value],
267) -> Option<Vec<FunctionOutputContentItem>> {
268 items
269 .iter()
270 .map(FunctionOutputContentItem::from_value)
271 .collect::<Option<Vec<_>>>()
272}
273
274pub(crate) fn parse_function_output_content_items_value(
275 value: &Value,
276) -> Option<Vec<FunctionOutputContentItem>> {
277 match value {
278 Value::Array(items) => parse_function_output_content_items_array(items),
279 Value::Object(obj) => ["content_items", "content", "output", "body"]
280 .iter()
281 .find_map(|key| obj.get(*key))
282 .and_then(parse_function_output_content_items_value),
283 Value::String(text) => parse_function_output_content_items_text(text),
284 Value::Null | Value::Bool(_) | Value::Number(_) => None,
285 }
286}
287
288pub(crate) fn parse_function_output_content_items_text(
289 text: &str,
290) -> Option<Vec<FunctionOutputContentItem>> {
291 let trimmed = text.trim();
292 if !(trimmed.starts_with('[') || trimmed.starts_with('{')) {
293 return None;
294 }
295 let parsed: Value = serde_json::from_str(trimmed).ok()?;
296 parse_function_output_content_items_value(&parsed)
297}
298
299fn function_output_items_from_parts(parts: &[ContentPart]) -> Vec<FunctionOutputContentItem> {
300 let mut items = Vec::new();
301 for part in parts {
302 match part {
303 ContentPart::Text { text } => {
304 if text.trim().is_empty() {
305 continue;
306 }
307 items.push(FunctionOutputContentItem::InputText { text: text.clone() });
308 }
309 ContentPart::Image {
310 data, mime_type, ..
311 } => {
312 items.push(FunctionOutputContentItem::InputImage {
313 image_url: format!("data:{};base64,{}", mime_type, data),
314 });
315 }
316 ContentPart::File { .. } => {}
317 }
318 }
319 items
320}
321
322fn function_output_value_from_items(items: Vec<FunctionOutputContentItem>) -> Value {
323 if items.is_empty() {
324 return Value::String(String::new());
325 }
326 let has_image = items
327 .iter()
328 .any(|item| matches!(item, FunctionOutputContentItem::InputImage { .. }));
329 if has_image {
330 return Value::Array(
331 items
332 .iter()
333 .map(FunctionOutputContentItem::to_function_output_json)
334 .collect(),
335 );
336 }
337 Value::String(text_from_function_output_items(&items).unwrap_or_default())
338}
339
340pub(crate) fn function_output_value_from_message_content(content: &MessageContent) -> Value {
341 match content {
342 MessageContent::Text(text) => {
343 if let Some(items) = parse_function_output_content_items_text(text) {
344 return function_output_value_from_items(items);
345 }
346 Value::String(text.clone())
347 }
348 MessageContent::Parts(parts) => {
349 let items = function_output_items_from_parts(parts);
350 function_output_value_from_items(items)
351 }
352 }
353}
354
355pub(crate) fn tool_result_content_from_message_content(content: &MessageContent) -> Vec<Value> {
356 match content {
357 MessageContent::Text(text) => {
358 if text.trim().is_empty() {
359 return Vec::new();
360 }
361 if let Some(items) = parse_function_output_content_items_text(text) {
362 return items
363 .iter()
364 .map(FunctionOutputContentItem::to_tool_result_json)
365 .collect();
366 }
367 vec![serde_json::json!({
368 "type": "output_text",
369 "text": text
370 })]
371 }
372 MessageContent::Parts(parts) => function_output_items_from_parts(parts)
373 .iter()
374 .map(FunctionOutputContentItem::to_tool_result_json)
375 .collect(),
376 }
377}
378
379fn text_from_function_output_items(items: &[FunctionOutputContentItem]) -> Option<String> {
380 let mut text = String::new();
381 for item in items {
382 match item {
383 FunctionOutputContentItem::InputText { text: segment } => text.push_str(segment),
384 FunctionOutputContentItem::InputImage { .. } => return None,
385 }
386 }
387 Some(text)
388}
389
390fn function_output_value_to_history_text(value: &Value) -> String {
391 if let Some(text) = value.as_str() {
392 return text.to_string();
393 }
394 if let Some(items) = parse_function_output_content_items_value(value)
395 && let Some(text) = text_from_function_output_items(&items)
396 {
397 return text;
398 }
399 if let Some(text) = value.get("content").and_then(Value::as_str) {
400 return text.to_string();
401 }
402 value.to_string()
403}
404
405fn append_output_item_text(value: &Value, text: &mut String) {
406 if let Some(part_text) = value.get("text").and_then(Value::as_str) {
407 text.push_str(part_text);
408 }
409 if let Some(part_output) = value.get("output").and_then(Value::as_str) {
410 text.push_str(part_output);
411 }
412 if let Some(refusal) = value.get("refusal").and_then(Value::as_str) {
413 text.push_str(refusal);
414 }
415
416 match value {
417 Value::String(s) => text.push_str(s),
418 Value::Array(parts) => {
419 for part in parts {
420 append_output_item_text(part, text);
421 }
422 }
423 Value::Object(_) => {
424 if let Some(content) = value.get("content") {
425 append_output_item_text(content, text);
426 }
427 }
428 _ => {}
429 }
430}
431
432fn output_item_text(content: &Value) -> String {
433 let mut text = String::new();
434 append_output_item_text(content, &mut text);
435 text
436}
437
438fn parse_function_call_item(item: &Value) -> Option<ToolCall> {
439 let function_obj = item.get("function").and_then(Value::as_object);
440 let namespace = item
441 .get("namespace")
442 .and_then(Value::as_str)
443 .or_else(|| function_obj.and_then(|f| f.get("namespace").and_then(Value::as_str)))
444 .map(ToOwned::to_owned);
445 let name = function_obj
446 .and_then(|f| f.get("name").and_then(Value::as_str))
447 .or_else(|| item.get("name").and_then(Value::as_str))?
448 .to_string();
449
450 let id = item
451 .get("id")
452 .and_then(Value::as_str)
453 .or_else(|| item.get("call_id").and_then(Value::as_str))
454 .filter(|value| !value.is_empty())
455 .unwrap_or("tool_call_compacted")
456 .to_string();
457
458 let arguments_value = function_obj
459 .and_then(|f| f.get("arguments"))
460 .or_else(|| item.get("arguments"));
461 let arguments = arguments_value.map_or_else(
462 || "{}".to_string(),
463 |value| {
464 value
465 .as_str()
466 .map(ToOwned::to_owned)
467 .unwrap_or_else(|| value.to_string())
468 },
469 );
470
471 Some(ToolCall::function_with_namespace(
472 id, namespace, name, arguments,
473 ))
474}
475
476fn parse_message_item(item: &Value) -> Option<Message> {
477 let role = item
478 .get("role")
479 .and_then(Value::as_str)
480 .unwrap_or("assistant");
481 let content_value = item.get("content").unwrap_or(&Value::Null);
482 let content = output_item_text(content_value).trim().to_string();
483
484 let tool_calls: Vec<ToolCall> = content_value
485 .as_array()
486 .into_iter()
487 .flatten()
488 .filter_map(|part| {
489 let part_type = part.get("type").and_then(Value::as_str).unwrap_or("");
490 if part_type == "function_call" || part_type == "tool_call" {
491 parse_function_call_item(part)
492 } else {
493 None
494 }
495 })
496 .collect();
497
498 let tool_result = content_value
499 .as_array()
500 .into_iter()
501 .flatten()
502 .find_map(|part| {
503 let part_type = part.get("type").and_then(Value::as_str).unwrap_or("");
504 if part_type != "tool_result" {
505 return None;
506 }
507
508 let tool_call_id = part
509 .get("tool_call_id")
510 .and_then(Value::as_str)
511 .or_else(|| item.get("tool_call_id").and_then(Value::as_str))
512 .or_else(|| item.get("call_id").and_then(Value::as_str))
513 .map(ToOwned::to_owned)?;
514
515 let tool_output = output_item_text(part.get("content").unwrap_or(&Value::Null))
516 .trim()
517 .to_string();
518 Some((tool_call_id, tool_output))
519 });
520
521 let assistant_phase = item
522 .get("phase")
523 .and_then(Value::as_str)
524 .and_then(AssistantPhase::from_wire_str);
525
526 match role {
527 "system" => Some(Message::system(content)),
528 "developer" => Some(Message::system(content)),
529 "user" => Some(Message::user(content)),
530 "assistant" => {
531 if tool_calls.is_empty() {
532 Some(Message::assistant(content).with_phase(assistant_phase))
533 } else {
534 Some(Message::assistant_with_tools(content, tool_calls).with_phase(assistant_phase))
535 }
536 }
537 "tool" => {
538 if let Some((tool_call_id, tool_output)) = tool_result {
539 return Some(Message::tool_response(tool_call_id, tool_output));
540 }
541
542 let tool_call_id = item
543 .get("tool_call_id")
544 .and_then(Value::as_str)
545 .or_else(|| item.get("call_id").and_then(Value::as_str))
546 .map(ToOwned::to_owned)?;
547 Some(Message::tool_response(tool_call_id, content))
548 }
549 _ => Some(Message {
550 role: MessageRole::Assistant,
551 content: MessageContent::text(content),
552 ..Message::default()
553 }),
554 }
555}
556
557#[inline]
558fn preserve_opaque_item(item: &Value) -> Message {
559 Message::assistant(String::new()).with_reasoning_details(Some(vec![item.clone()]))
560}
561
562pub(crate) fn parse_compacted_output_messages(output: &[Value]) -> Vec<Message> {
567 let mut messages = Vec::new();
568
569 for item in output {
570 let item_type = item.get("type").and_then(Value::as_str).unwrap_or("");
571 match item_type {
572 "message" => {
573 if let Some(message) = parse_message_item(item) {
574 messages.push(message);
575 } else {
576 messages.push(preserve_opaque_item(item));
577 }
578 }
579 "function_call" | "tool_call" => {
580 if let Some(tool_call) = parse_function_call_item(item) {
581 messages.push(Message::assistant_with_tools(
582 String::new(),
583 vec![tool_call],
584 ));
585 }
586 }
587 "function_call_output" => {
588 let call_id = item
589 .get("call_id")
590 .and_then(Value::as_str)
591 .or_else(|| item.get("id").and_then(Value::as_str))
592 .filter(|value| !value.is_empty());
593 if let Some(call_id) = call_id {
594 let output_text = item
595 .get("output")
596 .map(function_output_value_to_history_text)
597 .unwrap_or_default();
598 messages.push(Message::tool_response(call_id.to_string(), output_text));
599 } else {
600 messages.push(preserve_opaque_item(item));
601 }
602 }
603 _ => {
604 messages.push(preserve_opaque_item(item));
605 }
606 }
607 }
608
609 messages
610}
611
612pub struct StreamAggregator {
614 pub model: String,
615 pub content: String,
616 pub reasoning: String,
617 pub reasoning_details: Vec<String>,
618 pub reasoning_buffer: ReasoningBuffer,
619 pub tool_builders: Vec<ToolCallBuilder>,
620 pub usage: Option<crate::llm::provider::Usage>,
621 pub finish_reason: crate::llm::provider::FinishReason,
622 pub sanitizer: TagStreamSanitizer,
623 pub compaction: Option<String>,
624}
625
626#[derive(Clone, Copy, Debug, PartialEq, Eq)]
627pub enum OpenAiDeltaOrder {
628 ReasoningFirst,
629 ContentFirst,
630}
631
632fn emit_reasoning_delta(
633 aggregator: &mut StreamAggregator,
634 tx: &tokio::sync::mpsc::UnboundedSender<Result<LLMStreamEvent, LLMError>>,
635 delta: &Value,
636 reasoning_field: Option<&'static str>,
637) {
638 let Some(reasoning_field) = reasoning_field else {
639 return;
640 };
641 let Some(reasoning) = delta.get(reasoning_field).and_then(Value::as_str) else {
642 return;
643 };
644 let Some(delta) = aggregator.handle_reasoning(reasoning) else {
645 return;
646 };
647 let _ = tx.send(Ok(LLMStreamEvent::Reasoning { delta }));
648}
649
650fn emit_content_delta(
651 aggregator: &mut StreamAggregator,
652 tx: &tokio::sync::mpsc::UnboundedSender<Result<LLMStreamEvent, LLMError>>,
653 delta: &Value,
654) {
655 let Some(content) = delta.get("content").and_then(Value::as_str) else {
656 return;
657 };
658 for event in aggregator.handle_content(content) {
659 let _ = tx.send(Ok(event));
660 }
661}
662
663pub fn handle_openai_compatible_chunk(
664 value: &Value,
665 aggregator: &mut StreamAggregator,
666 tx: &tokio::sync::mpsc::UnboundedSender<Result<LLMStreamEvent, LLMError>>,
667 reasoning_field: Option<&'static str>,
668 delta_order: OpenAiDeltaOrder,
669) {
670 if let Some(choices) = value.get("choices").and_then(Value::as_array)
671 && let Some(choice) = choices.first()
672 {
673 if let Some(delta) = choice.get("delta") {
674 match delta_order {
675 OpenAiDeltaOrder::ReasoningFirst => {
676 emit_reasoning_delta(aggregator, tx, delta, reasoning_field);
677 emit_content_delta(aggregator, tx, delta);
678 }
679 OpenAiDeltaOrder::ContentFirst => {
680 emit_content_delta(aggregator, tx, delta);
681 emit_reasoning_delta(aggregator, tx, delta, reasoning_field);
682 }
683 }
684
685 if let Some(tool_calls) = delta.get("tool_calls").and_then(Value::as_array) {
686 aggregator.handle_tool_calls(tool_calls);
687 }
688 }
689
690 if let Some(reason) = choice.get("finish_reason").and_then(Value::as_str) {
691 aggregator.set_finish_reason(map_finish_reason_common(reason));
692 }
693 }
694
695 if let Some(_usage_value) = value.get("usage")
696 && let Some(usage) = crate::llm::providers::common::parse_usage_openai_format(value, true)
697 {
698 aggregator.set_usage(usage);
699 }
700}
701
702impl StreamAggregator {
703 pub fn new(model: String) -> Self {
704 Self {
705 model,
706 content: String::new(),
707 reasoning: String::new(),
708 reasoning_details: Vec::new(),
709 reasoning_buffer: ReasoningBuffer::default(),
710 tool_builders: Vec::new(),
711 usage: None,
712 finish_reason: crate::llm::provider::FinishReason::Stop,
713 sanitizer: TagStreamSanitizer::new(),
714 compaction: None,
715 }
716 }
717
718 pub fn handle_content(&mut self, delta: &str) -> Vec<LLMStreamEvent> {
720 self.content.push_str(delta);
721 self.sanitizer.process_chunk(delta)
722 }
723
724 pub fn handle_reasoning(&mut self, delta: &str) -> Option<String> {
726 let result = self.reasoning_buffer.push(delta);
727 if let Some(ref d) = result {
728 self.reasoning.push_str(d);
729 }
730 result
731 }
732
733 pub fn set_reasoning_details(&mut self, details: &[Value]) {
735 if details.is_empty() {
736 return;
737 }
738
739 self.reasoning_details = details
740 .iter()
741 .map(|detail| {
742 detail
743 .as_str()
744 .map(ToOwned::to_owned)
745 .unwrap_or_else(|| detail.to_string())
746 })
747 .collect();
748 }
749
750 pub fn handle_tool_calls(&mut self, deltas: &[Value]) {
752 update_tool_calls(&mut self.tool_builders, deltas);
753 }
754
755 pub fn set_usage(&mut self, usage: crate::llm::provider::Usage) {
757 self.usage = Some(usage);
758 }
759
760 pub fn set_finish_reason(&mut self, reason: crate::llm::provider::FinishReason) {
762 self.finish_reason = reason;
763 }
764
765 pub fn finalize(mut self) -> LLMResponse {
767 for event in self.sanitizer.finalize() {
769 match event {
770 LLMStreamEvent::Token { delta } => {
771 self.content.push_str(&delta);
772 }
773 LLMStreamEvent::Reasoning { delta } => {
774 self.reasoning.push_str(&delta);
775 }
776 _ => {}
777 }
778 }
779
780 let reasoning_details = if self.reasoning_details.is_empty() {
781 None
782 } else {
783 Some(self.reasoning_details)
784 };
785 let mut reasoning = if self.reasoning.is_empty() {
786 self.reasoning_buffer.finalize()
787 } else {
788 Some(self.reasoning)
789 };
790 if reasoning.is_none() {
791 reasoning = reasoning_details
792 .as_ref()
793 .and_then(|details| extract_reasoning_text_from_serialized_details(details));
794 }
795
796 LLMResponse {
797 content: if self.content.is_empty() {
798 None
799 } else {
800 Some(self.content)
801 },
802 tool_calls: finalize_tool_calls(self.tool_builders),
803 model: self.model,
804 usage: self.usage,
805 finish_reason: self.finish_reason,
806 reasoning,
807 reasoning_details,
808 tool_references: Vec::new(),
809 request_id: None,
810 organization_id: None,
811 compaction: self.compaction,
812 }
813 }
814}
815
816pub async fn process_openai_stream<S, E, F>(
821 mut byte_stream: S,
822 provider_name: &'static str,
823 model: String,
824 mut on_chunk: F,
825) -> Result<LLMResponse, LLMError>
826where
827 S: futures::Stream<Item = Result<bytes::Bytes, E>> + Unpin,
828 E: std::fmt::Display,
829 F: FnMut(Value) -> Result<(), LLMError>,
830{
831 use crate::llm::providers::error_handling::format_network_error;
832 use futures::StreamExt;
833
834 let mut buffer = String::new();
835 let mut last_response_value = None;
836
837 while let Some(chunk_result) = byte_stream.next().await {
838 let chunk_bytes =
839 chunk_result.map_err(|e| format_network_error(provider_name, &e.to_string()))?;
840 let chunk_str = String::from_utf8_lossy(&chunk_bytes);
841 buffer.push_str(&chunk_str);
842
843 while let Some((boundary_idx, boundary_len)) = find_sse_boundary(&buffer) {
844 let event = buffer[..boundary_idx].to_string();
845 buffer.drain(..boundary_idx + boundary_len);
846
847 if let Some(data) = extract_data_payload(&event) {
848 if data == "[DONE]" {
849 break;
850 }
851
852 for line in data.lines() {
853 let trimmed = line.trim();
854 if trimmed.is_empty() {
855 continue;
856 }
857
858 if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
859 on_chunk(value.clone())?;
860 last_response_value = Some(value);
861 }
862 }
863 }
864 }
865 }
866
867 let mut final_response = LLMResponse {
869 content: None,
870 tool_calls: None,
871 model,
872 usage: None,
873 finish_reason: crate::llm::provider::FinishReason::Stop,
874 reasoning: None,
875 reasoning_details: None,
876 tool_references: Vec::new(),
877 request_id: None,
878 organization_id: None,
879 compaction: None,
880 };
881
882 if let Some(value) = last_response_value
883 && value.get("usage").is_some()
884 {
885 final_response.usage =
886 crate::llm::providers::common::parse_usage_openai_format(&value, true);
887 }
888
889 Ok(final_response)
890}
891
892pub fn parse_openai_tool_calls(calls: &[Value]) -> Vec<ToolCall> {
893 calls
894 .iter()
895 .filter_map(|call| {
896 let id = call.get("id").and_then(|v| v.as_str())?;
897 let function = call.get("function")?;
898 let namespace = call
899 .get("namespace")
900 .and_then(|v| v.as_str())
901 .or_else(|| function.get("namespace").and_then(|v| v.as_str()))
902 .map(ToOwned::to_owned);
903 let name = function.get("name").and_then(|v| v.as_str())?;
904 let arguments = function.get("arguments");
905 let serialized = arguments.map_or_else(
906 || "{}".to_string(),
907 |value| {
908 if value.is_string() {
909 value.as_str().unwrap_or("").to_string()
910 } else {
911 value.to_string()
912 }
913 },
914 );
915 Some(ToolCall::function_with_namespace(
916 id.to_string(),
917 namespace,
918 name.to_string(),
919 serialized,
920 ))
921 })
922 .collect()
923}
924
925fn push_unique_tool_reference(tool_references: &mut Vec<String>, tool_name: &str) {
926 if !tool_references.iter().any(|existing| existing == tool_name) {
927 tool_references.push(tool_name.to_string());
928 }
929}
930
931pub(crate) fn collect_tool_references_from_tool_search_output(
932 value: &Value,
933 tool_references: &mut Vec<String>,
934) {
935 match value {
936 Value::Array(items) => {
937 for item in items {
938 collect_tool_references_from_tool_search_output(item, tool_references);
939 }
940 }
941 Value::Object(object) => {
942 if let Some(tools) = object.get("tools").and_then(Value::as_array) {
943 for tool in tools {
944 collect_tool_references_from_tool_search_output(tool, tool_references);
945 }
946 } else if let Some(tool_name) = object.get("tool_name").and_then(Value::as_str) {
947 push_unique_tool_reference(tool_references, tool_name);
948 } else if let Some(function) = object.get("function").and_then(Value::as_object)
949 && let Some(tool_name) = function.get("name").and_then(Value::as_str)
950 {
951 push_unique_tool_reference(tool_references, tool_name);
952 } else if let Some(tool_name) = object.get("name").and_then(Value::as_str) {
953 push_unique_tool_reference(tool_references, tool_name);
954 }
955
956 if let Some(tool_refs) = object.get("tool_references").and_then(Value::as_array) {
957 for tool_ref in tool_refs {
958 collect_tool_references_from_tool_search_output(tool_ref, tool_references);
959 }
960 }
961 }
962 _ => {}
963 }
964}
965
966pub fn append_text_with_reasoning(
967 text: &str,
968 aggregated_content: &mut String,
969 reasoning: &mut ReasoningBuffer,
970 deltas: &mut StreamDelta,
971 telemetry: &impl StreamTelemetry,
972) {
973 let (segments, cleaned) = split_reasoning_from_text(text);
974
975 if segments.is_empty() && cleaned.is_none() {
976 if !text.is_empty() {
977 aggregated_content.push_str(text);
978 deltas.push_content(text);
979 telemetry.on_content_delta(text);
980 }
981 return;
982 }
983
984 for segment in segments {
985 if let Some(stage) = &segment.stage {
986 telemetry.on_reasoning_stage(stage);
987 }
988 if let Some(delta) = reasoning.push(&segment.text) {
989 telemetry.on_reasoning_delta(&delta);
990 deltas.push_reasoning(&delta);
991 }
992 }
993
994 if let Some(cleaned_text) = cleaned
995 && !cleaned_text.is_empty()
996 {
997 aggregated_content.push_str(&cleaned_text);
998 telemetry.on_content_delta(&cleaned_text);
999 deltas.push_content(&cleaned_text);
1000 }
1001}
1002
1003#[inline]
1004pub fn extract_data_payload(event: &str) -> Option<String> {
1005 let mut out = String::new();
1006
1007 for raw_line in event.lines() {
1008 let line = raw_line.trim_end_matches('\r');
1009 if line.is_empty() || line.starts_with(':') {
1010 continue;
1011 }
1012
1013 if let Some(value) = line.strip_prefix("data:") {
1014 if !out.is_empty() {
1015 out.push('\n');
1016 }
1017 out.push_str(value.trim_start());
1018 }
1019 }
1020
1021 (!out.is_empty()).then_some(out)
1022}
1023
1024#[inline]
1025pub fn find_sse_boundary(buffer: &str) -> Option<(usize, usize)> {
1026 let newline_boundary = buffer.find("\n\n").map(|idx| (idx, 2));
1027 let carriage_boundary = buffer.find("\r\n\r\n").map(|idx| (idx, 4));
1028
1029 match (newline_boundary, carriage_boundary) {
1030 (Some((n_idx, n_len)), Some((c_idx, c_len))) => {
1031 if n_idx <= c_idx {
1032 Some((n_idx, n_len))
1033 } else {
1034 Some((c_idx, c_len))
1035 }
1036 }
1037 (Some(boundary), None) => Some(boundary),
1038 (None, Some(boundary)) => Some(boundary),
1039 (None, None) => None,
1040 }
1041}
1042
1043pub fn apply_tool_call_delta_from_content(
1044 builders: &mut Vec<ToolCallBuilder>,
1045 container: &Map<String, Value>,
1046 telemetry: &impl StreamTelemetry,
1047) {
1048 apply_tool_call_delta_with_index(builders, container, telemetry, None, None);
1049}
1050
1051fn apply_tool_call_delta_with_index(
1052 builders: &mut Vec<ToolCallBuilder>,
1053 container: &Map<String, Value>,
1054 telemetry: &impl StreamTelemetry,
1055 fallback_index: Option<usize>,
1056 fallback_id: Option<Value>,
1057) {
1058 fn extract_tool_call_id(container: &Map<String, Value>) -> Option<Value> {
1059 container.get("id").cloned().or_else(|| {
1060 container
1061 .get("tool_call")
1062 .and_then(|value| value.as_object())
1063 .and_then(|inner| inner.get("id"))
1064 .cloned()
1065 })
1066 }
1067
1068 let explicit_index = container
1069 .get("tool_call")
1070 .and_then(|value| value.as_object())
1071 .and_then(|tool_call| tool_call.get("index"))
1072 .and_then(|value| value.as_u64())
1073 .or_else(|| container.get("index").and_then(|value| value.as_u64()));
1074
1075 let index = explicit_index
1076 .map(|value| value as usize)
1077 .or(fallback_index)
1078 .unwrap_or(0);
1079
1080 let current_id = extract_tool_call_id(container).or_else(|| fallback_id.clone());
1081
1082 if let Some(nested) = container.get("delta").and_then(|value| value.as_object()) {
1083 apply_tool_call_delta_with_index(
1084 builders,
1085 nested,
1086 telemetry,
1087 Some(index),
1088 current_id.clone(),
1089 );
1090 }
1091
1092 let delta_source = container
1093 .get("tool_call")
1094 .and_then(|value| value.as_object())
1095 .unwrap_or(container);
1096
1097 let mut delta_map = Map::new();
1098
1099 if let Some(id_value) = extract_tool_call_id(delta_source).or_else(|| current_id.clone()) {
1100 delta_map.insert("id".to_string(), id_value);
1101 }
1102
1103 if let Some(function_value) = delta_source
1104 .get("function")
1105 .or_else(|| container.get("function"))
1106 {
1107 delta_map.insert("function".to_string(), function_value.clone());
1108 }
1109
1110 if delta_map.is_empty() {
1111 return;
1112 }
1113
1114 if builders.len() <= index {
1115 builders.resize_with(index + 1, ToolCallBuilder::default);
1116 }
1117
1118 let mut deltas = vec![Value::Null; index + 1];
1119 deltas[index] = Value::Object(delta_map);
1120 update_tool_calls(builders, &deltas);
1121 telemetry.on_tool_call_delta();
1122}
1123
1124#[cfg(test)]
1125mod tests {
1126 use super::*;
1127 use serde_json::json;
1128
1129 #[test]
1130 fn finalize_tool_calls_drops_empty_builders() {
1131 let builders = vec![ToolCallBuilder::default()];
1132 assert!(finalize_tool_calls(builders).is_none());
1133 }
1134
1135 #[test]
1136 fn append_text_with_reasoning_tracks_segments() {
1137 let telemetry = NoopStreamTelemetry;
1138 let mut aggregated = String::new();
1139 let mut reasoning = ReasoningBuffer::default();
1140 let mut delta = StreamDelta::default();
1141 append_text_with_reasoning(
1142 "Hello",
1143 &mut aggregated,
1144 &mut reasoning,
1145 &mut delta,
1146 &telemetry,
1147 );
1148 assert_eq!(aggregated, "Hello");
1149 assert_eq!(
1150 delta.into_fragments(),
1151 vec![StreamFragment::Content("Hello".into())]
1152 );
1153 }
1154
1155 #[test]
1156 fn apply_tool_call_delta_updates_builder() {
1157 let telemetry = NoopStreamTelemetry;
1158 let mut builders = Vec::new();
1159 let container = json!({
1160 "index": 0,
1161 "function": {"name": "foo", "arguments": "{}"}
1162 })
1163 .as_object()
1164 .cloned()
1165 .unwrap();
1166 apply_tool_call_delta_from_content(&mut builders, &container, &telemetry);
1167 let calls = finalize_tool_calls(builders).expect("call expected");
1168 let func = calls[0]
1169 .function
1170 .as_ref()
1171 .expect("function call should be present");
1172 assert_eq!(func.name, "foo");
1173 }
1174
1175 #[test]
1176 fn apply_tool_call_delta_uses_outer_index_for_nested_delta() {
1177 let telemetry = NoopStreamTelemetry;
1178 let mut builders = Vec::new();
1179 let container = json!({
1180 "delta": {
1181 "tool_call": {
1182 "function": {
1183 "name": "foo",
1184 "arguments": "{\"value\":1}"
1185 }
1186 }
1187 },
1188 "index": 1,
1189 "id": "call-1"
1190 })
1191 .as_object()
1192 .cloned()
1193 .unwrap();
1194
1195 apply_tool_call_delta_from_content(&mut builders, &container, &telemetry);
1196
1197 let calls = finalize_tool_calls(builders).expect("call expected");
1198 assert_eq!(calls.len(), 1);
1199 assert_eq!(calls[0].id, "call-1");
1200 let func = calls[0]
1201 .function
1202 .as_ref()
1203 .expect("function call should be present");
1204 assert_eq!(func.arguments, "{\"value\":1}");
1205 }
1206
1207 #[test]
1208 fn update_tool_calls_respects_explicit_index() {
1209 let mut builders = Vec::new();
1210 let deltas = vec![json!({
1211 "index": 2,
1212 "id": "call_3",
1213 "function": {
1214 "name": "get_weather",
1215 "arguments": "{\"city\":\"Beijing\"}"
1216 }
1217 })];
1218
1219 update_tool_calls(&mut builders, &deltas);
1220
1221 let calls = finalize_tool_calls(builders).expect("call expected");
1222 assert_eq!(calls.len(), 1);
1223 assert_eq!(calls[0].id, "call_3");
1224 let function = calls[0].function.as_ref().expect("function expected");
1225 assert_eq!(function.name, "get_weather");
1226 assert_eq!(function.arguments, "{\"city\":\"Beijing\"}");
1227 }
1228
1229 #[test]
1230 fn extract_data_payload_merges_lines() {
1231 let event = ": keep-alive\n".to_string() + "data: {\"a\":1}\n" + "data: {\"b\":2}\n";
1232 let payload = extract_data_payload(&event);
1233 assert_eq!(payload.as_deref(), Some("{\"a\":1}\n{\"b\":2}"));
1234 }
1235
1236 #[test]
1237 fn find_sse_boundary_prefers_newline() {
1238 let buffer = "data: foo\n\nrest";
1239 assert_eq!(find_sse_boundary(buffer), Some((9, 2)));
1240 }
1241
1242 #[test]
1243 fn parse_compacted_output_messages_keeps_messages() {
1244 let output = vec![json!({
1245 "type": "message",
1246 "role": "assistant",
1247 "phase": "final_answer",
1248 "content": [
1249 { "type": "output_text", "text": "Compacted response" }
1250 ]
1251 })];
1252
1253 let parsed = parse_compacted_output_messages(&output);
1254 assert_eq!(parsed.len(), 1);
1255 assert_eq!(parsed[0].role, MessageRole::Assistant);
1256 assert_eq!(parsed[0].phase, Some(AssistantPhase::FinalAnswer));
1257 assert_eq!(parsed[0].content.as_text(), "Compacted response");
1258 }
1259
1260 #[test]
1261 fn parse_compacted_output_messages_keeps_tool_pairs() {
1262 let output = vec![
1263 json!({
1264 "type": "function_call",
1265 "id": "call_1",
1266 "name": "shell",
1267 "arguments": "{\"command\":\"pwd\"}"
1268 }),
1269 json!({
1270 "type": "function_call_output",
1271 "call_id": "call_1",
1272 "output": "/tmp/work"
1273 }),
1274 ];
1275
1276 let parsed = parse_compacted_output_messages(&output);
1277 assert_eq!(parsed.len(), 2);
1278 assert_eq!(parsed[0].role, MessageRole::Assistant);
1279 assert!(parsed[0].tool_calls.is_some());
1280 assert_eq!(parsed[1].role, MessageRole::Tool);
1281 assert_eq!(parsed[1].tool_call_id.as_deref(), Some("call_1"));
1282 }
1283
1284 #[test]
1285 fn parse_compacted_output_messages_serializes_multimodal_function_output() {
1286 let output = vec![json!({
1287 "type": "function_call_output",
1288 "call_id": "call_1",
1289 "output": [
1290 { "type": "input_text", "text": "inline image note" },
1291 { "type": "input_image", "image_url": "data:image/png;base64,abc" }
1292 ]
1293 })];
1294
1295 let parsed = parse_compacted_output_messages(&output);
1296 assert_eq!(parsed.len(), 1);
1297 assert_eq!(parsed[0].role, MessageRole::Tool);
1298 assert_eq!(parsed[0].tool_call_id.as_deref(), Some("call_1"));
1299 let text = parsed[0].content.as_text();
1300 assert!(text.contains("\"input_image\""));
1301 assert!(text.contains("inline image note"));
1302 }
1303
1304 #[test]
1305 fn tool_result_content_parses_multimodal_tool_output_text() {
1306 let content = MessageContent::Text(
1307 r#"[{"type":"input_text","text":"note"},{"type":"input_image","image_url":"data:image/png;base64,abc"}]"#
1308 .to_string(),
1309 );
1310 let parts = tool_result_content_from_message_content(&content);
1311 assert_eq!(parts.len(), 2);
1312 assert_eq!(parts[0]["type"], "output_text");
1313 assert_eq!(parts[0]["text"], "note");
1314 assert_eq!(parts[1]["type"], "input_image");
1315 assert_eq!(parts[1]["image_url"], "data:image/png;base64,abc");
1316 }
1317
1318 #[test]
1319 fn function_output_value_parses_multimodal_tool_output_text() {
1320 let content = MessageContent::Text(
1321 r#"[{"type":"input_text","text":"note"},{"type":"input_image","image_url":"data:image/png;base64,abc"}]"#
1322 .to_string(),
1323 );
1324 let output = function_output_value_from_message_content(&content);
1325 let items = output.as_array().expect("expected array output");
1326 assert_eq!(items.len(), 2);
1327 assert_eq!(items[0]["type"], "input_text");
1328 assert_eq!(items[0]["text"], "note");
1329 assert_eq!(items[1]["type"], "input_image");
1330 assert_eq!(items[1]["image_url"], "data:image/png;base64,abc");
1331 }
1332
1333 #[test]
1334 fn parse_compacted_output_messages_preserves_compaction_items() {
1335 let output = vec![json!({
1336 "type": "compaction",
1337 "encrypted_content": "opaque_state"
1338 })];
1339
1340 let parsed = parse_compacted_output_messages(&output);
1341 assert_eq!(parsed.len(), 1);
1342 assert_eq!(parsed[0].role, MessageRole::Assistant);
1343 let preserved = parsed[0]
1344 .reasoning_details
1345 .as_ref()
1346 .and_then(|items| items.first())
1347 .and_then(|item| item.get("type"))
1348 .and_then(Value::as_str);
1349 assert_eq!(preserved, Some("compaction"));
1350 }
1351
1352 #[test]
1353 fn parse_compacted_output_messages_parses_tool_result_messages() {
1354 let output = vec![json!({
1355 "type": "message",
1356 "role": "tool",
1357 "content": [
1358 {
1359 "type": "tool_result",
1360 "tool_call_id": "call_42",
1361 "content": [
1362 { "type": "output_text", "text": "done" }
1363 ]
1364 }
1365 ]
1366 })];
1367
1368 let parsed = parse_compacted_output_messages(&output);
1369 assert_eq!(parsed.len(), 1);
1370 assert_eq!(parsed[0].role, MessageRole::Tool);
1371 assert_eq!(parsed[0].tool_call_id.as_deref(), Some("call_42"));
1372 assert_eq!(parsed[0].content.as_text(), "done");
1373 }
1374
1375 #[test]
1376 fn stream_aggregator_derives_reasoning_from_details_when_missing() {
1377 let mut aggregator = StreamAggregator::new("test-model".to_string());
1378 aggregator.set_reasoning_details(&[json!({
1379 "type": "reasoning.text",
1380 "text": "step one"
1381 })]);
1382
1383 let response = aggregator.finalize();
1384 assert_eq!(response.reasoning.as_deref(), Some("step one"));
1385 assert!(response.reasoning_details.is_some());
1386 }
1387}