1use crate::error::{Error, ProviderError};
6use crate::{
7 AssistantMessage, ContentBlock, Context, Model, ProviderEvent, StreamOptions, TextContent,
8 ToolCall,
9};
10use futures::StreamExt;
11
12pub async fn complete(
23 model: &Model,
24 context: &Context,
25 options: Option<StreamOptions>,
26) -> std::result::Result<AssistantMessage, Error> {
27 use crate::providers::stream;
28
29 let mut stream = stream(model, context, options).await?;
30
31 let mut final_message: Option<AssistantMessage> = None;
32 let mut text_buffer = String::new();
33 let mut current_text_index: Option<usize> = None;
34 let mut tool_calls: Vec<(usize, ToolCall)> = Vec::new();
35
36 while let Some(event) = stream.next().await {
37 match event {
38 ProviderEvent::Start { partial } => {
39 final_message = Some((*partial).clone());
40 }
41 ProviderEvent::TextStart {
42 content_index,
43 partial,
44 } => {
45 if final_message.is_none() {
46 final_message = Some((*partial).clone());
47 }
48 current_text_index = Some(content_index);
49 text_buffer.clear();
50 }
51 ProviderEvent::TextDelta {
52 delta,
53 content_index,
54 ..
55 } => {
56 if current_text_index != Some(content_index) {
57 if let Some(idx) = current_text_index
59 && !text_buffer.is_empty()
60 {
61 push_text_block(&mut final_message, idx, &text_buffer);
62 }
63 current_text_index = Some(content_index);
64 text_buffer.clear();
65 }
66 text_buffer.push_str(&delta);
67 }
68 ProviderEvent::TextEnd {
69 content_index,
70 content,
71 ..
72 } => {
73 push_text_block(&mut final_message, content_index, &content);
74 }
75 ProviderEvent::ThinkingStart {
76 content_index: _,
77 partial,
78 } => {
79 if final_message.is_none() {
80 final_message = Some((*partial).clone());
81 }
82 }
83 ProviderEvent::ThinkingDelta {
84 delta,
85 content_index,
86 ..
87 } => {
88 if let Some(ref mut msg) = final_message {
90 let content = ContentBlock::Thinking(crate::ThinkingContent {
92 content_type: crate::ThinkingContentType::Thinking,
93 thinking: delta,
94 thinking_signature: None,
95 redacted: None,
96 });
97 if content_index >= msg.content.len() {
98 msg.content.push(content);
99 }
100 }
101 }
102 ProviderEvent::ThinkingEnd {
103 content_index,
104 content,
105 ..
106 } => {
107 if let Some(ref mut msg) = final_message {
108 let thinking = ContentBlock::Thinking(crate::ThinkingContent {
109 content_type: crate::ThinkingContentType::Thinking,
110 thinking: content,
111 thinking_signature: None,
112 redacted: None,
113 });
114 if content_index >= msg.content.len() {
115 msg.content.push(thinking);
116 }
117 }
118 }
119 ProviderEvent::ToolCallStart {
120 content_index,
121 tool_call_id,
122 partial,
123 ..
124 } => {
125 if final_message.is_none() {
126 final_message = Some((*partial).clone());
127 }
128 let id = tool_call_id.unwrap_or_else(|| format!("tool_call_{}", content_index));
130 let tc = ToolCall {
131 content_type: crate::ToolCallType::ToolCall,
132 id,
133 name: String::new(),
134 arguments: serde_json::json!({}),
135 thought_signature: None,
136 };
137 tool_calls.push((content_index, tc));
138 }
139 ProviderEvent::ToolCallDelta {
140 delta,
141 content_index,
142 ..
143 } => {
144 if let Some((_, tc)) = tool_calls.iter_mut().find(|(idx, _)| *idx == content_index)
146 {
147 let current_args = tc.arguments.to_string() + δ
149 if let Ok(parsed) = serde_json::from_str(¤t_args) {
150 tc.arguments = parsed;
151 }
152 }
153 }
154 ProviderEvent::ToolCallEnd {
155 content_index,
156 tool_call,
157 ..
158 } => {
159 if let Some((_, tc)) = tool_calls.iter_mut().find(|(idx, _)| *idx == content_index)
161 {
162 *tc = tool_call.clone();
163 }
164 push_tool_call(&mut final_message, content_index, tool_call.clone());
166 }
167 ProviderEvent::Done { message, .. } => {
168 if let Some(idx) = current_text_index
170 && !text_buffer.is_empty()
171 {
172 push_text_block(&mut final_message, idx, &text_buffer);
173 }
174
175 for (content_index, tc) in &tool_calls {
177 push_tool_call(&mut final_message, *content_index, tc.clone());
178 }
179
180 final_message = Some(message);
181 break;
182 }
183 ProviderEvent::Error { error, .. } => {
184 return Err(Error::Provider(ProviderError::StreamError(
185 error
186 .error_message
187 .unwrap_or_else(|| "Unknown error".to_string()),
188 )));
189 }
190 ProviderEvent::ImageStart { .. }
193 | ProviderEvent::ImageDelta { .. }
194 | ProviderEvent::ImageEnd { .. } => {}
195 }
196 }
197
198 final_message.ok_or_else(|| {
199 Error::Provider(ProviderError::StreamError(
200 "Stream ended without message".to_string(),
201 ))
202 })
203}
204
205fn push_text_block(msg: &mut Option<AssistantMessage>, index: usize, text: &str) {
207 if let Some(m) = msg {
208 let content = ContentBlock::Text(TextContent {
209 content_type: crate::TextContentType::Text,
210 text: text.to_string(),
211 text_signature: None,
212 });
213
214 while m.content.len() <= index {
216 m.content.push(ContentBlock::Text(TextContent {
217 content_type: crate::TextContentType::Text,
218 text: String::new(),
219 text_signature: None,
220 }));
221 }
222
223 if let ContentBlock::Text(t) = &mut m.content[index] {
225 if t.text.is_empty() {
226 *t = TextContent::new(text);
227 } else {
228 t.text.push_str(text);
229 }
230 } else {
231 m.content[index] = content;
232 }
233 }
234}
235
236fn push_tool_call(msg: &mut Option<AssistantMessage>, index: usize, tool_call: ToolCall) {
238 if let Some(m) = msg {
239 while m.content.len() <= index {
240 m.content.push(ContentBlock::Text(TextContent::new("")));
241 }
242 m.content[index] = ContentBlock::ToolCall(tool_call);
243 }
244}
245
246pub mod tokens {
248 pub fn estimate(text: &str) -> usize {
277 if text.is_empty() {
278 return 0;
279 }
280
281 let mut cjk_chars: usize = 0;
282 let mut ascii_or_latin_chars: usize = 0;
283 let mut punct_chars: usize = 0;
284 let mut whitespace_words: usize = 0;
285 let mut in_word = false;
286
287 for ch in text.chars() {
288 if ch.is_whitespace() {
289 if in_word {
290 whitespace_words += 1;
291 in_word = false;
292 }
293 } else {
294 in_word = true;
295 if is_cjk(ch) {
296 cjk_chars += 1;
297 } else if is_punctuation(ch) {
298 punct_chars += 1;
299 } else {
300 ascii_or_latin_chars += 1;
301 }
302 }
303 }
304 if in_word {
306 whitespace_words += 1;
307 }
308
309 let cjk_tokens = cjk_chars;
311 let punct_tokens = (punct_chars * 3).div_ceil(2);
313 let ascii_tokens = ascii_or_latin_chars.div_ceil(4);
315 let ws_tokens = whitespace_words / 8;
317
318 cjk_tokens + punct_tokens + ascii_tokens + ws_tokens
319 }
320
321 fn is_cjk(ch: char) -> bool {
323 matches!(ch,
324 '\u{4E00}'..='\u{9FFF}' | '\u{3400}'..='\u{4DBF}' | '\u{20000}'..='\u{2A6DF}' | '\u{2A700}'..='\u{2B73F}' | '\u{2B740}'..='\u{2B81F}' | '\u{F900}'..='\u{FAFF}' | '\u{2F800}'..='\u{2FA1F}' | '\u{3000}'..='\u{303F}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{AC00}'..='\u{D7AF}' )
336 }
337
338 fn is_punctuation(ch: char) -> bool {
341 ch.is_ascii_punctuation()
342 || matches!(
343 ch,
344 '\u{201C}'
345 | '\u{201D}'
346 | '\u{2018}'
347 | '\u{2019}'
348 | '\u{2026}'
349 | '\u{2013}'
350 | '\u{2014}'
351 | '\u{00AB}'
352 | '\u{00BB}'
353 | '\u{00B7}'
354 | '\u{2022}'
355 | '\u{203B}'
356 | '\u{2192}'
357 | '\u{2190}'
358 | '\u{21D2}'
359 | '\u{2194}'
360 | '\\'
361 | '|'
362 | '~'
363 | '^'
364 | '`'
365 )
366 }
367
368 pub fn estimate_words(text: &str) -> usize {
379 let word_count = text.split_whitespace().count();
380 let per_word = if text.chars().any(is_cjk) { 1.6 } else { 1.3 };
382 (word_count as f64 * per_word) as usize
383 }
384
385 pub fn context_usage(text: &str, context_window: usize) -> f64 {
394 if context_window == 0 {
395 return 0.0;
396 }
397 (estimate(text) as f64 / context_window as f64).min(1.0)
398 }
399
400 #[cfg(test)]
401 mod tests {
402 use super::*;
403
404 #[test]
405 fn estimate_empty_string() {
406 assert_eq!(estimate(""), 0);
407 }
408
409 #[test]
410 fn estimate_plain_english() {
411 let tokens = estimate("Hello world, this is a test.");
413 assert!(
415 (4..=14).contains(&tokens),
416 "expected 4–14 tokens for plain English sentence, got {}",
417 tokens
418 );
419 }
420
421 #[test]
422 fn estimate_cjk() {
423 let tokens = estimate("\u{4F60}\u{597D}\u{4E16}\u{754C}\u{6D4B}\u{8BD5}");
425 assert!(
426 tokens >= 4,
427 "expected >= 4 tokens for 5 CJK chars, got {}",
428 tokens
429 );
430 }
431
432 #[test]
433 fn estimate_code() {
434 let code = "fn main() { println!(\"hello\"); }";
435 let tokens = estimate(code);
436 assert!(
438 (4..=20).contains(&tokens),
439 "expected 4–20 tokens for code snippet, got {}",
440 tokens
441 );
442 }
443
444 #[test]
445 fn estimate_longer_than_naive() {
446 let text = "{ \"key\": \"value\" }";
449 let hybrid = estimate(text);
450 let naive = text.len() / 4;
451 assert!(hybrid > 0);
453 assert!(hybrid <= naive * 10, "hybrid={} naive={}", hybrid, naive);
456 }
457
458 #[test]
459 fn context_usage_clamped() {
460 assert_eq!(context_usage("short", 0), 0.0);
461 assert!(context_usage("hello", 100000) < 1.0);
462 }
463 }
464}
465
466pub use tokens::estimate as estimate_tokens;