magi-code 0.61.0

Repository-aware CLI coding agent for terminal work
Documentation
use super::chat_completions::chat_tool_call_response_item;
use super::*;
use serde_json::Value;

impl StreamParser {
    pub(super) fn reasoning_summary_delta_from_event<'a>(
        &self,
        value: &'a Value,
        item_type: &str,
    ) -> Option<&'a str> {
        (item_type == "response.reasoning_summary_text.delta")
            .then(|| value.pointer("/delta").and_then(Value::as_str))
            .flatten()
            .or_else(|| {
                value
                    .pointer("/choices/0/delta/reasoning_content")
                    .and_then(Value::as_str)
            })
            .or_else(|| {
                value
                    .pointer("/choices/0/message/reasoning_content")
                    .and_then(Value::as_str)
            })
    }

    pub(super) fn reasoning_summary_done_from_event<'a>(
        &self,
        value: &'a Value,
        item_type: &str,
    ) -> Option<(&'a str, Option<&'a str>)> {
        if item_type != "response.reasoning_summary_text.done" {
            return None;
        }
        let summary = value.pointer("/text").and_then(Value::as_str)?;
        let item_id = value
            .pointer("/item/id")
            .and_then(Value::as_str)
            .or_else(|| value.get("item_id").and_then(Value::as_str))
            .or_else(|| value.get("id").and_then(Value::as_str));
        Some((summary, item_id))
    }

    pub(super) fn reconcile_reasoning_summary_complete(
        &mut self,
        summary: &str,
        item_id: Option<&str>,
    ) -> Vec<ProviderEvent> {
        if summary.trim().is_empty() {
            return Vec::new();
        }
        let key = item_id
            .filter(|id| !id.trim().is_empty())
            .map(|id| format!("id:{id}"))
            .unwrap_or_else(|| "legacy".to_string());
        if item_id.is_some() {
            self.saw_identified_reasoning_completion = true;
        }
        if self
            .completed_reasoning_summary_keys
            .get(&key)
            .is_some_and(|previous| previous == summary)
        {
            return Vec::new();
        }
        self.completed_reasoning_summary_keys
            .insert(key, summary.to_string());

        let mut events = Vec::new();
        if item_id.is_none() {
            if let Some(suffix) = summary.strip_prefix(&self.reasoning_summary_text) {
                if !suffix.is_empty() {
                    self.reasoning_summary_text.push_str(suffix);
                    events.push(ProviderEvent::ReasoningSummaryDelta(suffix.to_string()));
                }
            } else {
                self.reasoning_summary_text.clear();
                self.reasoning_summary_text.push_str(summary);
            }
        }

        if let Some(item_id) = item_id {
            events.push(ProviderEvent::ReasoningSummaryCompleteIdentified(
                ReasoningSummary {
                    text: summary.to_string(),
                    item_id: Some(item_id.to_string()),
                    turn_id: None,
                },
            ));
        } else {
            events.push(ProviderEvent::ReasoningSummaryComplete(summary.to_string()));
        }
        events
    }

    // qwen/vLLM embeds reasoning as inline text ending with </think> (no opening <think> tag).
    // Buffer chat content until </think> or terminal flush: there is no safe threshold to
    // distinguish reasoning from normal text without the closing marker. Reasoning length is
    // unbounded (verified >6KB in live capture), so any byte threshold either breaks long
    // reasoning or adds latency. Non-thinking models accept buffering until stream end.
    pub(super) fn process_chat_content_delta(
        &mut self,
        delta: &str,
    ) -> (Option<String>, Option<String>) {
        if self.thinking_complete {
            return (None, (!delta.is_empty()).then(|| delta.to_string()));
        }

        self.content_buffer.push_str(delta);

        if let Some(close_index) = self.content_buffer.find("</think>") {
            let reasoning = self.content_buffer[..close_index].to_string();
            let visible_start = close_index + "</think>".len();
            let visible = self.content_buffer[visible_start..].to_string();
            self.content_buffer.clear();
            self.thinking_complete = true;
            return (
                (!reasoning.is_empty()).then_some(reasoning),
                (!visible.is_empty()).then_some(visible),
            );
        }

        (None, None)
    }

    pub(super) fn flush_pending_chat_content(&mut self, events: &mut Vec<ProviderEvent>) {
        if self.content_buffer.is_empty() {
            return;
        }
        let text = std::mem::take(&mut self.content_buffer);
        // ponytail: if </think> never arrived, treat buffered content as visible text.
        self.thinking_complete = true;

        // Gemma 4 / vLLM diffusion models sometimes emit tool calls inline in content
        // using the Hermes-style format: <|tool_call>call:NAME{ARGS}<tool_call|>
        // where <|"|> is an escaped double quote. Parse these before emitting as text.
        if self.gemma_inline_tool_calls_enabled
            && let Some(tool_calls) = self.parse_gemma_inline_tool_calls(&text)
        {
            events.push(ProviderEvent::ResponseItem(chat_tool_call_response_item(
                &tool_calls,
            )));
            events.extend(tool_calls.into_iter().map(ProviderEvent::ToolCall));
        } else {
            self.emitted_text_delta = true;
            events.push(ProviderEvent::TextDelta(text));
        }
    }
}

pub(super) fn reasoning_summary_text(item: &Value) -> Option<String> {
    if item.get("type").and_then(Value::as_str) != Some("reasoning") {
        return None;
    }
    let parts = item
        .get("summary")
        .and_then(Value::as_array)?
        .iter()
        .filter(|part| part.get("type").and_then(Value::as_str) == Some("summary_text"))
        .filter_map(|part| part.get("text").and_then(Value::as_str))
        .filter(|text| !text.trim().is_empty())
        .map(ToString::to_string)
        .collect::<Vec<_>>();
    (!parts.is_empty()).then(|| parts.join("\n"))
}