codex_convert_proxy/convert/
util.rs1use crate::constants::MAX_THINKING_BUFFER_SIZE;
4use crate::types::response_api::{OutputItemType, Tool, ToolType};
5
6use super::streaming::ResponseRequestContext;
7use memchr::memmem;
8
9pub fn map_tool_name_to_output_type(
11 tool_name: &str,
12 original_tools: Option<&Vec<Tool>>,
13) -> OutputItemType {
14 if let Some(tools) = original_tools {
15 for t in tools {
16 if t.name.as_deref() == Some(tool_name) {
17 return match t.tool_type {
18 ToolType::WebSearchPreview => OutputItemType::WebSearchCall,
19 ToolType::FileSearch => OutputItemType::FileSearchCall,
20 _ => OutputItemType::FunctionCall,
21 };
22 }
23 }
24 }
25 match tool_name {
26 "web_search_preview" | "web_search" => OutputItemType::WebSearchCall,
27 "file_search" => OutputItemType::FileSearchCall,
28 _ => OutputItemType::FunctionCall,
29 }
30}
31
32pub fn map_tool_name_to_stream_item_type(
34 tool_name: &str,
35 request_context: Option<&ResponseRequestContext>,
36) -> String {
37 let tools = request_context.map(|ctx| &ctx.tools);
38 match map_tool_name_to_output_type(tool_name, tools) {
39 OutputItemType::WebSearchCall => "web_search_call".to_string(),
40 OutputItemType::FileSearchCall => "file_search_call".to_string(),
41 _ => "function_call".to_string(),
42 }
43}
44
45pub fn extract_queries_from_arguments(arguments: &str) -> Option<Vec<String>> {
47 if let Ok(value) = serde_json::from_str::<serde_json::Value>(arguments) {
48 if let Some(query) = value.get("query").and_then(|v| v.as_str()) {
49 return Some(vec![query.to_string()]);
50 }
51 if let Some(queries) = value.get("queries").and_then(|v| v.as_array()) {
52 let qs: Vec<String> = queries
53 .iter()
54 .filter_map(|q| q.as_str().map(|s| s.to_string()))
55 .collect();
56 if !qs.is_empty() {
57 return Some(qs);
58 }
59 }
60 }
61 None
62}
63
64pub fn parse_thought_tags(content: &str) -> (String, Option<String>) {
69 let mut actual_content = String::new();
70 let mut reasoning_parts: Vec<String> = Vec::new();
71 let mut remaining = content;
72
73 loop {
74 let thought_start = remaining.find("<thought>");
75 let think_start = remaining.find("<think>");
76
77 let (start_idx, open_tag, close_tag) = match (thought_start, think_start) {
78 (Some(t), Some(k)) => {
79 if t <= k {
80 (t, "<thought>", "</thought>")
81 } else {
82 (k, "<think>", "</think>")
83 }
84 }
85 (Some(t), None) => (t, "<thought>", "</thought>"),
86 (None, Some(k)) => (k, "<think>", "</think>"),
87 (None, None) => break,
88 };
89
90 actual_content.push_str(&remaining[..start_idx]);
91
92 let after_start = &remaining[start_idx + open_tag.len()..];
93 if let Some(end_idx) = after_start.find(close_tag) {
94 let reasoning_content = &after_start[..end_idx];
95 if !reasoning_content.is_empty() {
96 reasoning_parts.push(reasoning_content.to_string());
97 }
98 remaining = &after_start[end_idx + close_tag.len()..];
99 } else {
100 actual_content.push_str(&remaining[start_idx..]);
101 remaining = "";
102 break;
103 }
104 }
105
106 actual_content.push_str(remaining);
107
108 let reasoning = if reasoning_parts.is_empty() {
109 None
110 } else {
111 Some(reasoning_parts.join("\n\n"))
112 };
113
114 (actual_content.trim().to_string(), reasoning)
115}
116
117pub fn parse_streaming_thinking(
122 text: &str,
123 is_thinking: bool,
124 buffer: &mut String,
125) -> (String, Option<String>, bool) {
126 let mut actual_text = String::new();
127 let mut reasoning = String::new();
128 let mut current_is_thinking = is_thinking;
129
130 buffer.push_str(text);
131
132 if buffer.len() > MAX_THINKING_BUFFER_SIZE {
133 let flushed = buffer.clone();
137 buffer.clear();
138 return (String::new(), Some(flushed), is_thinking);
139 }
140
141 let full_content = buffer.clone();
142 buffer.clear();
143
144 let bytes = full_content.as_bytes();
145 let mut pos = 0;
146
147 while pos < bytes.len() {
148 if current_is_thinking {
149 let think_close = memmem::find(&bytes[pos..], b"</think>");
150 let thought_close = memmem::find(&bytes[pos..], b"</thought>");
151
152 match (think_close, thought_close) {
153 (Some(close_pos), Some(thought_close_pos)) => {
154 if close_pos <= thought_close_pos {
155 let content = std::str::from_utf8(&bytes[pos..pos + close_pos]).unwrap_or("");
156 reasoning.push_str(content);
157 pos += close_pos + 8;
158 current_is_thinking = false;
159 } else {
160 let content = std::str::from_utf8(&bytes[pos..pos + thought_close_pos]).unwrap_or("");
161 reasoning.push_str(content);
162 pos += thought_close_pos + 10;
163 current_is_thinking = false;
164 }
165 }
166 (Some(close_pos), None) => {
167 let content = std::str::from_utf8(&bytes[pos..pos + close_pos]).unwrap_or("");
168 reasoning.push_str(content);
169 pos += close_pos + 8;
170 current_is_thinking = false;
171 }
172 (None, Some(thought_close_pos)) => {
173 let content = std::str::from_utf8(&bytes[pos..pos + thought_close_pos]).unwrap_or("");
174 reasoning.push_str(content);
175 pos += thought_close_pos + 10;
176 current_is_thinking = false;
177 }
178 (None, None) => {
179 let remaining = std::str::from_utf8(&bytes[pos..]).unwrap_or("");
180 buffer.push_str(remaining);
181 break;
182 }
183 }
184 } else {
185 let think_open = memmem::find(&bytes[pos..], b"<think>");
186 let thought_open = memmem::find(&bytes[pos..], b"<thought>");
187
188 match (think_open, thought_open) {
189 (Some(open_pos), Some(thought_open_pos)) => {
190 if open_pos <= thought_open_pos {
191 let content = std::str::from_utf8(&bytes[pos..pos + open_pos]).unwrap_or("");
192 actual_text.push_str(content);
193 pos += open_pos + 7;
194 current_is_thinking = true;
195 } else {
196 let content = std::str::from_utf8(&bytes[pos..pos + thought_open_pos]).unwrap_or("");
197 actual_text.push_str(content);
198 pos += thought_open_pos + 9;
199 current_is_thinking = true;
200 }
201 }
202 (Some(open_pos), None) => {
203 let content = std::str::from_utf8(&bytes[pos..pos + open_pos]).unwrap_or("");
204 actual_text.push_str(content);
205 pos += open_pos + 7;
206 current_is_thinking = true;
207 }
208 (None, Some(thought_open_pos)) => {
209 let content = std::str::from_utf8(&bytes[pos..pos + thought_open_pos]).unwrap_or("");
210 actual_text.push_str(content);
211 pos += thought_open_pos + 9;
212 current_is_thinking = true;
213 }
214 (None, None) => {
215 let remaining = std::str::from_utf8(&bytes[pos..]).unwrap_or("");
216 actual_text.push_str(remaining);
217 break;
218 }
219 }
220 }
221 }
222
223 let reasoning_delta = if reasoning.is_empty() {
224 None
225 } else {
226 Some(reasoning)
227 };
228
229 (actual_text, reasoning_delta, current_is_thinking)
230}
231
232pub fn find_pattern(chars: &[char], start: usize, pattern: &[char]) -> Option<usize> {
234 if start + pattern.len() > chars.len() {
235 return None;
236 }
237 for i in start..=chars.len() - pattern.len() {
238 if chars[i..i + pattern.len()] == *pattern {
239 return Some(i);
240 }
241 }
242 None
243}
244pub fn sanitize_pseudo_tool_markup(text: &str) -> String {
246 text.replace("<request_user_input", "<request_user_input")
247 .replace("</request_user_input>", "</request_user_input>")
248 .replace("<parameter ", "<parameter ")
249 .replace("</parameter>", "</parameter>")
250}