Skip to main content

vtcode_core/llm/providers/shared/
mod.rs

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
562/// Convert `/responses/compact` output items into VT Code message history.
563///
564/// Opaque/unmapped items are preserved in `reasoning_details` so they can be
565/// forwarded back to Responses-compatible providers on subsequent turns.
566pub(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
612/// Helper to aggregate streaming events and produce a final LLMResponse.
613pub 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    /// Process a content delta, applying sanitization for reasoning tags.
719    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    /// Process a reasoning delta from a dedicated field.
725    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    /// Store structured reasoning details received from streaming deltas.
734    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    /// Process tool call deltas.
751    pub fn handle_tool_calls(&mut self, deltas: &[Value]) {
752        update_tool_calls(&mut self.tool_builders, deltas);
753    }
754
755    /// Set usage metrics.
756    pub fn set_usage(&mut self, usage: crate::llm::provider::Usage) {
757        self.usage = Some(usage);
758    }
759
760    /// Set finish reason.
761    pub fn set_finish_reason(&mut self, reason: crate::llm::provider::FinishReason) {
762        self.finish_reason = reason;
763    }
764
765    /// Finalize and produce the completed LLMResponse.
766    pub fn finalize(mut self) -> LLMResponse {
767        // Collect any leftover bits from sanitizer
768        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
816/// Common helper for processing OpenAI-compatible SSE streams.
817///
818/// This simplifies stream implementations across providers like DeepSeek, ZAI, Moonshot, etc.
819/// Especially optimized for high-performance models like Gemini 3 and GLM-5.
820pub 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    // Attempt to extract final response metadata (usage, etc) from last chunk if not already done
868    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}