use serde_json::{json, Map, Value}; const THINK_OPEN_TAG: &str = ""; const THINK_CLOSE_TAG: &str = ""; // 穷举上游可能的 reasoning 回传字段,优先级:reasoning_content > reasoning(字符串/对象) > reasoning_details。 // 不依赖 provider meta 的 outputFormat 声明,因此对各家 Chat 兼容接口都能兜底提取。 pub(crate) fn extract_reasoning_field_text(value: &Value) -> Option { for key in ["reasoning_content", "reasoning"] { if let Some(text) = value.get(key).and_then(|v| v.as_str()) { if !text.is_empty() { return Some(text.to_string()); } } } if let Some(reasoning) = value.get("reasoning") { for key in ["content", "text", "summary"] { if let Some(text) = reasoning.get(key).and_then(|v| v.as_str()) { if !text.is_empty() { return Some(text.to_string()); } } } } if let Some(details) = value.get("reasoning_details") { if let Some(text) = extract_reasoning_details_text(details) { return Some(text); } } None } fn extract_reasoning_details_text(value: &Value) -> Option { match value { Value::String(text) => (!text.is_empty()).then(|| text.to_string()), Value::Array(parts) => { let text = parts .iter() .filter_map(extract_reasoning_detail_part_text) .filter(|text| !text.is_empty()) .collect::>() .join("\n\n"); (!text.is_empty()).then_some(text) } Value::Object(_) => extract_reasoning_detail_part_text(value), _ => None, } } fn extract_reasoning_detail_part_text(value: &Value) -> Option { for key in ["text", "content", "summary"] { if let Some(text) = value.get(key).and_then(|v| v.as_str()) { if !text.is_empty() { return Some(text.to_string()); } } } if let Some(parts) = value.get("parts").and_then(|v| v.as_array()) { let text = parts .iter() .filter_map(extract_reasoning_detail_part_text) .filter(|text| !text.is_empty()) .collect::>() .join("\n\n"); return (!text.is_empty()).then_some(text); } None } pub(crate) fn extract_reasoning_summary_text(value: &Value) -> Option { for key in ["reasoning_content", "content", "text"] { if let Some(text) = value.get(key).and_then(|v| v.as_str()) { if !text.is_empty() { return Some(text.to_string()); } } } let summary = value.get("summary")?; if let Some(text) = summary.as_str() { return (!text.is_empty()).then(|| text.to_string()); } let parts = summary.as_array()?; let text = parts .iter() .filter_map(|part| { part.get("text") .and_then(|v| v.as_str()) .or_else(|| part.get("content").and_then(|v| v.as_str())) .or_else(|| part.as_str()) }) .filter(|text| !text.is_empty()) .collect::>() .join("\n\n"); (!text.is_empty()).then_some(text) } pub(crate) fn append_reasoning_content(message: &mut Map, reasoning: &str) -> bool { let reasoning = reasoning.trim(); if reasoning.is_empty() { return false; } match message.get_mut("reasoning_content") { Some(Value::String(existing)) if !existing.is_empty() => { existing.push_str("\n\n"); existing.push_str(reasoning); } _ => { message.insert( "reasoning_content".to_string(), Value::String(reasoning.to_string()), ); } } true } pub(crate) fn attach_reasoning_content_field(item: &mut Value, reasoning: &str) -> bool { let reasoning = reasoning.trim(); if reasoning.is_empty() { return false; } if let Some(obj) = item.as_object_mut() { obj.insert( "reasoning_content".to_string(), Value::String(reasoning.to_string()), ); return true; } false } pub(crate) fn attach_optional_reasoning_content_field( item: &mut Value, reasoning: Option<&str>, ) -> bool { let Some(reasoning) = reasoning else { return false; }; attach_reasoning_content_field(item, reasoning) } pub(crate) fn response_function_call_item( item_id: &str, status: &str, call_id: &str, name: &str, arguments: &str, reasoning: Option<&str>, ) -> Value { let mut item = json!({ "id": item_id, "type": "function_call", "status": status, "call_id": call_id, "name": name, "arguments": arguments }); attach_optional_reasoning_content_field(&mut item, reasoning); item } pub(crate) fn response_item_call_id(item: &Value) -> Option { item.get("call_id") .or_else(|| item.get("id")) .and_then(|value| value.as_str()) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToString::to_string) } pub(crate) fn is_empty_value(value: &Value) -> bool { match value { Value::Null => true, Value::String(value) => value.trim().is_empty(), Value::Array(value) => value.is_empty(), Value::Object(value) => value.is_empty(), _ => false, } } pub(crate) fn split_leading_think_block(text: &str) -> Option<(String, String)> { let leading_ws_len = text.len() - text.trim_start().len(); let after_ws = &text[leading_ws_len..]; if !after_ws.starts_with(THINK_OPEN_TAG) { return None; } let body_start = leading_ws_len + THINK_OPEN_TAG.len(); let close_relative = text[body_start..].find(THINK_CLOSE_TAG)?; let close_start = body_start + close_relative; let answer_start = close_start + THINK_CLOSE_TAG.len(); Some(( text[body_start..close_start].trim().to_string(), strip_think_answer_separator(&text[answer_start..]).to_string(), )) } pub(crate) fn strip_leading_think_open_tag(text: &str) -> Option { let leading_ws_len = text.len() - text.trim_start().len(); let after_ws = &text[leading_ws_len..]; after_ws .strip_prefix(THINK_OPEN_TAG) .map(|value| value.trim().to_string()) } fn strip_think_answer_separator(text: &str) -> &str { text.trim_start_matches(['\r', '\n', '\t', ' ']) }