1use crate::config::Config;
2use crate::database::ChatEntry;
3use crate::model_metadata::MetadataExtractor;
4use crate::provider::{
5 ChatRequest, Message, MessageContent, OpenAIClient,
6};
7use crate::token_utils::TokenCounter;
8use anyhow::Result;
9use chrono::{DateTime, Utc};
10
11pub async fn send_chat_request_with_validation(
12 client: &LLMClient,
13 model: &str,
14 prompt: &str,
15 history: &[ChatEntry],
16 system_prompt: Option<&str>,
17 max_tokens: Option<u32>,
18 temperature: Option<f32>,
19 provider_name: &str,
20 tools: Option<Vec<crate::provider::Tool>>,
21) -> Result<(String, Option<i32>, Option<i32>)> {
22 crate::debug_log!("Sending chat request - provider: '{}', model: '{}', prompt length: {}, history entries: {}",
23 provider_name, model, prompt.len(), history.len());
24 crate::debug_log!(
25 "Request parameters - max_tokens: {:?}, temperature: {:?}",
26 max_tokens,
27 temperature
28 );
29
30 crate::debug_log!(
32 "Loading model metadata for provider '{}', model '{}'",
33 provider_name,
34 model
35 );
36 let model_metadata = get_model_metadata(provider_name, model).await;
37
38 if let Some(ref metadata) = model_metadata {
39 crate::debug_log!(
40 "Found metadata for model '{}' - context_length: {:?}, max_output: {:?}",
41 model,
42 metadata.context_length,
43 metadata.max_output_tokens
44 );
45 } else {
46 crate::debug_log!("No metadata found for model '{}'", model);
47 }
48
49 crate::debug_log!("Creating token counter for model '{}'", model);
51 let token_counter = match TokenCounter::new(model) {
52 Ok(counter) => {
53 crate::debug_log!("Successfully created token counter for model '{}'", model);
54 Some(counter)
55 }
56 Err(e) => {
57 crate::debug_log!(
58 "Failed to create token counter for model '{}': {}",
59 model,
60 e
61 );
62 eprintln!(
63 "Warning: Failed to create token counter for model '{}': {}",
64 model, e
65 );
66 None
67 }
68 };
69
70 let mut final_prompt = prompt.to_string();
71 let mut final_history = history.to_vec();
72 let mut input_tokens = None;
73
74 if let (Some(metadata), Some(ref counter)) = (&model_metadata, &token_counter) {
76 if let Some(context_limit) = metadata.context_length {
77 if counter.exceeds_context_limit(prompt, system_prompt, history, context_limit) {
79 println!(
80 "⚠️ Input exceeds model context limit ({}k tokens). Truncating...",
81 context_limit / 1000
82 );
83
84 let (truncated_prompt, truncated_history) = counter.truncate_to_fit(
86 prompt,
87 system_prompt,
88 history,
89 context_limit,
90 metadata.max_output_tokens,
91 );
92
93 final_prompt = truncated_prompt;
94 final_history = truncated_history;
95
96 if final_history.len() < history.len() {
97 println!(
98 "📝 Truncated conversation history from {} to {} messages",
99 history.len(),
100 final_history.len()
101 );
102 }
103
104 if final_prompt.len() < prompt.len() {
105 println!(
106 "✂️ Truncated prompt from {} to {} characters",
107 prompt.len(),
108 final_prompt.len()
109 );
110 }
111 }
112
113 input_tokens = Some(counter.estimate_chat_tokens(
115 &final_prompt,
116 system_prompt,
117 &final_history,
118 ) as i32);
119 }
120 } else if let Some(ref counter) = token_counter {
121 input_tokens =
123 Some(counter.estimate_chat_tokens(&final_prompt, system_prompt, &final_history) as i32);
124 }
125
126 let mut messages = Vec::new();
128
129 if let Some(sys_prompt) = system_prompt {
131 messages.push(Message {
132 role: "system".to_string(),
133 content_type: MessageContent::Text {
134 content: Some(sys_prompt.to_string()),
135 },
136 tool_calls: None,
137 tool_call_id: None,
138 });
139 }
140
141 for entry in &final_history {
143 messages.push(Message::user(entry.question.clone()));
144 messages.push(Message::assistant(entry.response.clone()));
145 }
146
147 messages.push(Message::user(final_prompt));
149
150 let request = ChatRequest {
151 model: model.to_string(),
152 messages: messages.clone(),
153 max_tokens: max_tokens.or(Some(1024)),
154 temperature: temperature.or(Some(0.7)),
155 tools,
156 stream: None, };
158
159 crate::debug_log!(
160 "Sending chat request with {} messages, max_tokens: {:?}, temperature: {:?}",
161 messages.len(),
162 request.max_tokens,
163 request.temperature
164 );
165
166 crate::debug_log!("Making API call to chat endpoint...");
168 let response = client.chat(&request).await?;
169
170 crate::debug_log!(
171 "Received response from chat API ({} characters)",
172 response.len()
173 );
174
175 let output_tokens = if let Some(ref counter) = token_counter {
177 Some(counter.count_tokens(&response) as i32)
178 } else {
179 None
180 };
181
182 if let (Some(input), Some(output)) = (input_tokens, output_tokens) {
184 println!(
185 "📊 Token usage: {} input + {} output = {} total",
186 input,
187 output,
188 input + output
189 );
190
191 if let Some(metadata) = &model_metadata {
193 if let (Some(input_price), Some(output_price)) =
194 (metadata.input_price_per_m, metadata.output_price_per_m)
195 {
196 let input_cost = (input as f64 / 1_000_000.0) * input_price;
197 let output_cost = (output as f64 / 1_000_000.0) * output_price;
198 let total_cost = input_cost + output_cost;
199 println!(
200 "💰 Estimated cost: ${:.6} (${:.6} input + ${:.6} output)",
201 total_cost, input_cost, output_cost
202 );
203 }
204 }
205 }
206
207 Ok((response, input_tokens, output_tokens))
208}
209
210pub async fn send_chat_request_with_streaming(
211 client: &LLMClient,
212 model: &str,
213 prompt: &str,
214 history: &[ChatEntry],
215 system_prompt: Option<&str>,
216 max_tokens: Option<u32>,
217 temperature: Option<f32>,
218 provider_name: &str,
219 tools: Option<Vec<crate::provider::Tool>>,
220) -> Result<()> {
221 crate::debug_log!("Sending streaming chat request - provider: '{}', model: '{}', prompt length: {}, history entries: {}",
222 provider_name, model, prompt.len(), history.len());
223 crate::debug_log!(
224 "Request parameters - max_tokens: {:?}, temperature: {:?}",
225 max_tokens,
226 temperature
227 );
228
229 crate::debug_log!(
231 "Loading model metadata for provider '{}', model '{}'",
232 provider_name,
233 model
234 );
235 let model_metadata = get_model_metadata(provider_name, model).await;
236
237 if let Some(ref metadata) = model_metadata {
238 crate::debug_log!(
239 "Found metadata for model '{}' - context_length: {:?}, max_output: {:?}",
240 model,
241 metadata.context_length,
242 metadata.max_output_tokens
243 );
244 } else {
245 crate::debug_log!("No metadata found for model '{}'", model);
246 }
247
248 crate::debug_log!("Creating token counter for model '{}'", model);
250 let token_counter = match TokenCounter::new(model) {
251 Ok(counter) => {
252 crate::debug_log!("Successfully created token counter for model '{}'", model);
253 Some(counter)
254 }
255 Err(e) => {
256 crate::debug_log!(
257 "Failed to create token counter for model '{}': {}",
258 model,
259 e
260 );
261 eprintln!(
262 "Warning: Failed to create token counter for model '{}': {}",
263 model, e
264 );
265 None
266 }
267 };
268
269 let mut final_prompt = prompt.to_string();
270 let mut final_history = history.to_vec();
271
272 if let (Some(metadata), Some(ref counter)) = (&model_metadata, &token_counter) {
274 if let Some(context_limit) = metadata.context_length {
275 if counter.exceeds_context_limit(prompt, system_prompt, history, context_limit) {
277 println!(
278 "⚠️ Input exceeds model context limit ({}k tokens). Truncating...",
279 context_limit / 1000
280 );
281
282 let (truncated_prompt, truncated_history) = counter.truncate_to_fit(
284 prompt,
285 system_prompt,
286 history,
287 context_limit,
288 metadata.max_output_tokens,
289 );
290
291 final_prompt = truncated_prompt;
292 final_history = truncated_history;
293
294 if final_history.len() < history.len() {
295 println!(
296 "📝 Truncated conversation history from {} to {} messages",
297 history.len(),
298 final_history.len()
299 );
300 }
301
302 if final_prompt.len() < prompt.len() {
303 println!(
304 "✂️ Truncated prompt from {} to {} characters",
305 prompt.len(),
306 final_prompt.len()
307 );
308 }
309 }
310 }
311 }
312
313 let mut messages = Vec::new();
315
316 if let Some(sys_prompt) = system_prompt {
318 messages.push(Message {
319 role: "system".to_string(),
320 content_type: MessageContent::Text {
321 content: Some(sys_prompt.to_string()),
322 },
323 tool_calls: None,
324 tool_call_id: None,
325 });
326 }
327
328 for entry in &final_history {
330 messages.push(Message::user(entry.question.clone()));
331 messages.push(Message::assistant(entry.response.clone()));
332 }
333
334 messages.push(Message::user(final_prompt));
336
337 let request = ChatRequest {
338 model: model.to_string(),
339 messages: messages.clone(),
340 max_tokens: max_tokens.or(Some(1024)),
341 temperature: temperature.or(Some(0.7)),
342 tools,
343 stream: Some(true), };
345
346 crate::debug_log!(
347 "Sending streaming chat request with {} messages, max_tokens: {:?}, temperature: {:?}",
348 messages.len(),
349 request.max_tokens,
350 request.temperature
351 );
352
353 crate::debug_log!("Making streaming API call to chat endpoint...");
355 client.chat_stream(&request).await?;
356
357 Ok(())
358}
359
360async fn get_model_metadata(
361 provider_name: &str,
362 model_name: &str,
363) -> Option<crate::model_metadata::ModelMetadata> {
364 use std::fs;
365
366 let filename = format!("models/{}.json", provider_name);
367
368 if !std::path::Path::new(&filename).exists() {
369 return None;
370 }
371
372 match fs::read_to_string(&filename) {
373 Ok(json_content) => {
374 match MetadataExtractor::extract_from_provider(provider_name, &json_content) {
375 Ok(models) => models.into_iter().find(|m| m.id == model_name),
376 Err(_) => None,
377 }
378 }
379 Err(_) => None,
380 }
381}
382
383pub async fn get_or_refresh_token(
384 config: &mut Config,
385 provider_name: &str,
386 client: &OpenAIClient,
387) -> Result<String> {
388 let provider = config.get_provider(provider_name)?.clone();
390 let is_vertex = provider
391 .endpoint
392 .to_lowercase()
393 .contains("aiplatform.googleapis.com")
394 || provider.auth_type.as_deref() == Some("google_sa_jwt");
395
396 if let Some(cached_token) = config.get_cached_token(provider_name) {
398 if Utc::now() < cached_token.expires_at {
399 return Ok(cached_token.token.clone());
400 }
401 }
402
403 if is_vertex {
404 let token_url = provider
406 .token_url
407 .clone()
408 .unwrap_or_else(|| "https://oauth2.googleapis.com/token".to_string());
409
410 let api_key_raw = provider.api_key.clone().ok_or_else(|| {
412 anyhow::anyhow!(
413 "Service Account JSON not set for '{}'. Run lc k a {} and paste SA JSON.",
414 provider_name,
415 provider_name
416 )
417 })?;
418 #[derive(serde::Deserialize)]
419 struct GoogleSA {
420 #[serde(rename = "type")]
421 sa_type: String,
422 client_email: String,
423 private_key: String,
424 }
425 let sa: GoogleSA = serde_json::from_str(&api_key_raw)
426 .map_err(|e| anyhow::anyhow!("Invalid Service Account JSON: {}", e))?;
427 if sa.sa_type != "service_account" {
428 anyhow::bail!("Provided key is not a service_account");
429 }
430
431 #[derive(serde::Serialize)]
433 struct Claims<'a> {
434 iss: &'a str,
435 scope: &'a str,
436 aud: &'a str,
437 exp: i64,
438 iat: i64,
439 }
440 let now = Utc::now().timestamp();
441 let claims = Claims {
442 iss: &sa.client_email,
443 scope: "https://www.googleapis.com/auth/cloud-platform",
444 aud: &token_url,
445 iat: now,
446 exp: now + 3600,
447 };
448 let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
449 let key = jsonwebtoken::EncodingKey::from_rsa_pem(sa.private_key.as_bytes())
450 .map_err(|e| anyhow::anyhow!("Failed to load RSA key: {}", e))?;
451 let assertion = jsonwebtoken::encode(&header, &claims, &key)
452 .map_err(|e| anyhow::anyhow!("JWT encode failed: {}", e))?;
453
454 #[derive(serde::Deserialize)]
456 struct GoogleTokenResp {
457 access_token: String,
458 expires_in: i64,
459 #[allow(dead_code)]
460 token_type: String,
461 }
462 let http = reqwest::Client::new();
463 let resp = http
464 .post(&token_url)
465 .form(&[
466 ("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
467 ("assertion", assertion.as_str()),
468 ])
469 .send()
470 .await
471 .map_err(|e| anyhow::anyhow!("Token exchange error: {}", e))?;
472 if !resp.status().is_success() {
473 let status = resp.status();
474 let txt = resp.text().await.unwrap_or_default();
475 anyhow::bail!("Token exchange failed ({}): {}", status, txt);
476 }
477 let token_json: GoogleTokenResp = resp
478 .json()
479 .await
480 .map_err(|e| anyhow::anyhow!("Failed to parse token response: {}", e))?;
481 let expires_at = DateTime::from_timestamp(now + token_json.expires_in - 60, 0)
482 .ok_or_else(|| anyhow::anyhow!("Invalid expires timestamp"))?;
483 config.set_cached_token(
484 provider_name.to_string(),
485 token_json.access_token.clone(),
486 expires_at,
487 )?;
488 config.save()?;
489 return Ok(token_json.access_token);
490 }
491
492 let token_url = match config.get_token_url(provider_name) {
494 Some(url) => url.clone(),
495 None => {
496 let provider_config = config.get_provider(provider_name)?;
497 return provider_config.api_key.clone().ok_or_else(|| {
498 anyhow::anyhow!(
499 "No API key or token URL configured for provider '{}'",
500 provider_name
501 )
502 });
503 }
504 };
505
506 let token_response = client.get_token_from_url(&token_url).await?;
507 let expires_at = DateTime::from_timestamp(token_response.expires_at, 0).ok_or_else(|| {
508 anyhow::anyhow!(
509 "Invalid expires_at timestamp: {}",
510 token_response.expires_at
511 )
512 })?;
513 config.set_cached_token(
514 provider_name.to_string(),
515 token_response.token.clone(),
516 expires_at,
517 )?;
518 config.save()?;
519 Ok(token_response.token)
520}
521
522
523pub type LLMClient = OpenAIClient;
525
526pub async fn create_authenticated_client(
529 config: &mut Config,
530 provider_name: &str,
531) -> Result<LLMClient> {
532 crate::debug_log!(
533 "Creating authenticated client for provider '{}'",
534 provider_name
535 );
536 let provider_config = config.get_provider(provider_name)?.clone();
537
538 crate::debug_log!(
539 "Provider '{}' config - endpoint: {}, models_path: {}, chat_path: {}",
540 provider_name,
541 provider_config.endpoint,
542 provider_config.models_path,
543 provider_config.chat_path
544 );
545
546 let normalized_chat_path = provider_config.chat_path.replace("{model_name}", "{model}");
548 let provider_config = crate::config::ProviderConfig {
549 chat_path: normalized_chat_path,
550 ..provider_config
551 };
552
553 let needs_oauth = provider_config
556 .endpoint
557 .contains("aiplatform.googleapis.com")
558 || provider_config.auth_type.as_deref() == Some("google_sa_jwt");
559
560 if needs_oauth {
561 let temp_client = OpenAIClient::new_with_headers(
563 provider_config.endpoint.clone(),
564 provider_config.api_key.clone().unwrap_or_default(),
565 provider_config.models_path.clone(),
566 provider_config.chat_path.clone(),
567 provider_config.headers.clone(),
568 );
569
570 let auth_token = get_or_refresh_token(config, provider_name, &temp_client).await?;
571
572 let mut oauth_headers = provider_config.headers.clone();
574 oauth_headers.insert(
575 "Authorization".to_string(),
576 format!("Bearer {}", auth_token),
577 );
578
579 let client = OpenAIClient::new_with_provider_config(
580 provider_config.endpoint.clone(),
581 auth_token,
582 provider_config.models_path.clone(),
583 provider_config.chat_path.clone(),
584 oauth_headers,
585 provider_config.clone(),
586 );
587
588 return Ok(client);
589 }
590
591 let temp_client = OpenAIClient::new_with_headers(
593 provider_config.endpoint.clone(),
594 provider_config.api_key.clone().unwrap_or_default(),
595 provider_config.models_path.clone(),
596 provider_config.chat_path.clone(),
597 provider_config.headers.clone(),
598 );
599
600 let auth_token = get_or_refresh_token(config, provider_name, &temp_client).await?;
601
602 let client = OpenAIClient::new_with_provider_config(
603 provider_config.endpoint.clone(),
604 auth_token,
605 provider_config.models_path.clone(),
606 provider_config.chat_path.clone(),
607 provider_config.headers.clone(),
608 provider_config.clone(),
609 );
610
611 Ok(client)
612}
613
614pub async fn send_chat_request_with_tool_execution(
616 client: &LLMClient,
617 model: &str,
618 prompt: &str,
619 history: &[ChatEntry],
620 system_prompt: Option<&str>,
621 max_tokens: Option<u32>,
622 temperature: Option<f32>,
623 _provider_name: &str,
624 tools: Option<Vec<crate::provider::Tool>>,
625 mcp_server_names: &[&str],
626) -> Result<(String, Option<i32>, Option<i32>)> {
627 use crate::provider::{ChatRequest, Message};
628 use crate::token_utils::TokenCounter;
629
630 let mut conversation_messages = Vec::new();
631 let mut total_input_tokens = 0i32;
632 let mut total_output_tokens = 0i32;
633
634 let token_counter = TokenCounter::new(model).ok();
636
637 if let Some(sys_prompt) = system_prompt {
639 conversation_messages.push(Message {
640 role: "system".to_string(),
641 content_type: MessageContent::Text {
642 content: Some(sys_prompt.to_string()),
643 },
644 tool_calls: None,
645 tool_call_id: None,
646 });
647 }
648
649 for entry in history {
651 conversation_messages.push(Message::user(entry.question.clone()));
652 conversation_messages.push(Message::assistant(entry.response.clone()));
653 }
654
655 conversation_messages.push(Message::user(prompt.to_string()));
657 let max_iterations = 10; let mut iteration = 0;
659
660 loop {
661 iteration += 1;
662 if iteration > max_iterations {
663 anyhow::bail!(
664 "Maximum tool execution iterations reached ({})",
665 max_iterations
666 );
667 }
668
669 crate::debug_log!("Tool execution iteration {}/{}", iteration, max_iterations);
670
671 let request = ChatRequest {
672 model: model.to_string(),
673 messages: conversation_messages.clone(),
674 max_tokens: max_tokens.or(Some(1024)),
675 temperature: temperature.or(Some(0.7)),
676 tools: tools.clone(),
677 stream: None, };
679
680 let response = client.chat_with_tools(&request).await?;
682
683 if let Some(ref counter) = token_counter {
685 let input_tokens = counter.estimate_chat_tokens("", system_prompt, &[]) as i32;
686 total_input_tokens += input_tokens;
687 }
688
689 if let Some(choice) = response.choices.first() {
690 crate::debug_log!(
691 "Response choice - tool_calls: {}, content: {}",
692 choice.message.tool_calls.as_ref().map_or(0, |tc| tc.len()),
693 choice
694 .message
695 .content
696 .as_ref()
697 .map_or("None", |c| if c.len() > 50 { &c[..50] } else { c })
698 );
699
700 if let Some(tool_calls) = &choice.message.tool_calls {
702 if !tool_calls.is_empty() {
703 crate::debug_log!(
704 "LLM made {} tool calls in iteration {}",
705 tool_calls.len(),
706 iteration
707 );
708
709 conversation_messages
711 .push(Message::assistant_with_tool_calls(tool_calls.clone()));
712
713 for (i, tool_call) in tool_calls.iter().enumerate() {
715 crate::debug_log!(
716 "Executing tool call {}/{}: {} with args: {}",
717 i + 1,
718 tool_calls.len(),
719 tool_call.function.name,
720 tool_call.function.arguments
721 );
722
723 let daemon_client = crate::mcp_daemon::DaemonClient::new()?;
725 let mut tool_result = None;
726 for server_name in mcp_server_names {
727 let args_value: serde_json::Value =
729 serde_json::from_str(&tool_call.function.arguments)?;
730 match daemon_client
731 .call_tool(server_name, &tool_call.function.name, args_value)
732 .await
733 {
734 Ok(result) => {
735 crate::debug_log!(
736 "Tool call successful on server '{}': {}",
737 server_name,
738 serde_json::to_string(&result)
739 .unwrap_or_else(|_| "invalid json".to_string())
740 );
741 tool_result = Some(format_tool_result(&result));
742 break;
743 }
744 Err(e) => {
745 crate::debug_log!(
746 "Tool call failed on server '{}': {}",
747 server_name,
748 e
749 );
750 continue;
751 }
752 }
753 }
754
755 let result_content = tool_result.unwrap_or_else(|| {
756 format!(
757 "Error: Function '{}' not found on any MCP server",
758 tool_call.function.name
759 )
760 });
761
762 crate::debug_log!(
763 "Tool result for {}: {}",
764 tool_call.function.name,
765 if result_content.len() > 100 {
766 format!("{}...", &result_content[..100])
767 } else {
768 result_content.clone()
769 }
770 );
771
772 conversation_messages
774 .push(Message::tool_result(tool_call.id.clone(), result_content));
775 }
776
777 continue;
779 } else {
780 if let Some(content) = &choice.message.content {
782 if !content.trim().is_empty() {
783 crate::debug_log!("LLM provided final answer with empty tool_calls after {} iterations: {}",
784 iteration, if content.len() > 100 {
785 format!("{}...", &content[..100])
786 } else {
787 content.clone()
788 });
789
790 if let Some(ref counter) = token_counter {
792 total_output_tokens += counter.count_tokens(content) as i32;
793 }
794
795 return Ok((
797 content.clone(),
798 Some(total_input_tokens),
799 Some(total_output_tokens),
800 ));
801 }
802 }
803 }
804 } else if let Some(content) = &choice.message.content {
805 crate::debug_log!(
807 "LLM provided final answer without tool_calls field after {} iterations: {}",
808 iteration,
809 if content.len() > 100 {
810 format!("{}...", &content[..100])
811 } else {
812 content.clone()
813 }
814 );
815
816 if let Some(ref counter) = token_counter {
818 total_output_tokens += counter.count_tokens(content) as i32;
819 }
820
821 return Ok((
823 content.clone(),
824 Some(total_input_tokens),
825 Some(total_output_tokens),
826 ));
827 } else {
828 crate::debug_log!(
830 "LLM provided neither tool calls nor content in iteration {}",
831 iteration
832 );
833 anyhow::bail!(
834 "No content or tool calls in response from LLM in iteration {}",
835 iteration
836 );
837 }
838 } else {
839 anyhow::bail!("No response from API");
840 }
841 }
842}
843
844fn format_tool_result(result: &serde_json::Value) -> String {
846 if let Some(content_array) = result.get("content") {
847 if let Some(content_items) = content_array.as_array() {
848 let mut formatted = String::new();
849 for item in content_items {
850 if let Some(text) = item.get("text") {
851 if let Some(text_str) = text.as_str() {
852 formatted.push_str(text_str);
853 formatted.push('\n');
854 }
855 }
856 }
857 return formatted.trim().to_string();
858 }
859 }
860
861 serde_json::to_string_pretty(result).unwrap_or_else(|_| "Error formatting result".to_string())
863}
864
865pub async fn send_chat_request_with_validation_messages(
868 client: &LLMClient,
869 model: &str,
870 messages: &[Message],
871 system_prompt: Option<&str>,
872 max_tokens: Option<u32>,
873 temperature: Option<f32>,
874 provider_name: &str,
875 tools: Option<Vec<crate::provider::Tool>>,
876) -> Result<(String, Option<i32>, Option<i32>)> {
877 crate::debug_log!(
878 "Sending chat request with messages - provider: '{}', model: '{}', messages: {}",
879 provider_name,
880 model,
881 messages.len()
882 );
883
884 let mut final_messages = Vec::new();
886
887 if let Some(sys_prompt) = system_prompt {
889 let has_system = messages.iter().any(|m| m.role == "system");
890 if !has_system {
891 final_messages.push(Message {
892 role: "system".to_string(),
893 content_type: MessageContent::Text {
894 content: Some(sys_prompt.to_string()),
895 },
896 tool_calls: None,
897 tool_call_id: None,
898 });
899 }
900 }
901
902 final_messages.extend_from_slice(messages);
904
905 let request = ChatRequest {
906 model: model.to_string(),
907 messages: final_messages,
908 max_tokens: max_tokens.or(Some(1024)),
909 temperature: temperature.or(Some(0.7)),
910 tools,
911 stream: None,
912 };
913
914 let response = client.chat(&request).await?;
915
916 Ok((response, None, None))
918}
919
920pub async fn send_chat_request_with_streaming_messages(
921 client: &LLMClient,
922 model: &str,
923 messages: &[Message],
924 system_prompt: Option<&str>,
925 max_tokens: Option<u32>,
926 temperature: Option<f32>,
927 provider_name: &str,
928 tools: Option<Vec<crate::provider::Tool>>,
929) -> Result<()> {
930 crate::debug_log!(
931 "Sending streaming chat request with messages - provider: '{}', model: '{}', messages: {}",
932 provider_name,
933 model,
934 messages.len()
935 );
936
937 let mut final_messages = Vec::new();
939
940 if let Some(sys_prompt) = system_prompt {
942 let has_system = messages.iter().any(|m| m.role == "system");
943 if !has_system {
944 final_messages.push(Message {
945 role: "system".to_string(),
946 content_type: MessageContent::Text {
947 content: Some(sys_prompt.to_string()),
948 },
949 tool_calls: None,
950 tool_call_id: None,
951 });
952 }
953 }
954
955 final_messages.extend_from_slice(messages);
957
958 let request = ChatRequest {
959 model: model.to_string(),
960 messages: final_messages,
961 max_tokens: max_tokens.or(Some(1024)),
962 temperature: temperature.or(Some(0.7)),
963 tools,
964 stream: Some(true),
965 };
966
967 client.chat_stream(&request).await?;
968
969 Ok(())
970}
971
972pub async fn send_chat_request_with_tool_execution_messages(
973 client: &LLMClient,
974 model: &str,
975 messages: &[Message],
976 system_prompt: Option<&str>,
977 max_tokens: Option<u32>,
978 temperature: Option<f32>,
979 provider_name: &str,
980 tools: Option<Vec<crate::provider::Tool>>,
981 mcp_server_names: &[&str],
982) -> Result<(String, Option<i32>, Option<i32>)> {
983 crate::debug_log!("Sending chat request with tool execution and messages - provider: '{}', model: '{}', messages: {}",
984 provider_name, model, messages.len());
985
986 let mut conversation_messages = Vec::new();
987
988 if let Some(sys_prompt) = system_prompt {
990 let has_system = messages.iter().any(|m| m.role == "system");
991 if !has_system {
992 conversation_messages.push(Message {
993 role: "system".to_string(),
994 content_type: MessageContent::Text {
995 content: Some(sys_prompt.to_string()),
996 },
997 tool_calls: None,
998 tool_call_id: None,
999 });
1000 }
1001 }
1002
1003 conversation_messages.extend_from_slice(messages);
1005
1006 let max_iterations = 10;
1007 let mut iteration = 0;
1008
1009 loop {
1010 iteration += 1;
1011 if iteration > max_iterations {
1012 anyhow::bail!(
1013 "Maximum tool execution iterations reached ({})",
1014 max_iterations
1015 );
1016 }
1017
1018 let request = ChatRequest {
1019 model: model.to_string(),
1020 messages: conversation_messages.clone(),
1021 max_tokens: max_tokens.or(Some(1024)),
1022 temperature: temperature.or(Some(0.7)),
1023 tools: tools.clone(),
1024 stream: None,
1025 };
1026
1027 let response = client.chat_with_tools(&request).await?;
1028
1029 if let Some(choice) = response.choices.first() {
1030 if let Some(tool_calls) = &choice.message.tool_calls {
1031 if !tool_calls.is_empty() {
1032 conversation_messages
1033 .push(Message::assistant_with_tool_calls(tool_calls.clone()));
1034
1035 for tool_call in tool_calls {
1036 let daemon_client = crate::mcp_daemon::DaemonClient::new()?;
1037 let mut tool_result = None;
1038
1039 for server_name in mcp_server_names {
1040 let args_value: serde_json::Value =
1041 serde_json::from_str(&tool_call.function.arguments)?;
1042 match daemon_client
1043 .call_tool(server_name, &tool_call.function.name, args_value)
1044 .await
1045 {
1046 Ok(result) => {
1047 tool_result = Some(format_tool_result(&result));
1048 break;
1049 }
1050 Err(_) => continue,
1051 }
1052 }
1053
1054 let result_content = tool_result.unwrap_or_else(|| {
1055 format!(
1056 "Error: Function '{}' not found on any MCP server",
1057 tool_call.function.name
1058 )
1059 });
1060
1061 conversation_messages
1062 .push(Message::tool_result(tool_call.id.clone(), result_content));
1063 }
1064
1065 continue;
1066 }
1067 }
1068
1069 if let Some(content) = &choice.message.content {
1070 return Ok((content.clone(), None, None));
1071 }
1072 }
1073
1074 anyhow::bail!("No response from API");
1075 }
1076}