lc/
chat.rs

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    // Try to get model metadata for context validation
31    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    // Create token counter
50    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    // Validate context size if we have both metadata and token counter
75    if let (Some(metadata), Some(ref counter)) = (&model_metadata, &token_counter) {
76        if let Some(context_limit) = metadata.context_length {
77            // Check if input exceeds context limit
78            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                // Truncate to fit within context limit
85                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            // Calculate input tokens after potential truncation
114            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        // No metadata available, but we can still count tokens
122        input_tokens =
123            Some(counter.estimate_chat_tokens(&final_prompt, system_prompt, &final_history) as i32);
124    }
125
126    // Build messages for the request
127    let mut messages = Vec::new();
128
129    // Add system prompt if provided
130    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    // Add conversation history
142    for entry in &final_history {
143        messages.push(Message::user(entry.question.clone()));
144        messages.push(Message::assistant(entry.response.clone()));
145    }
146
147    // Add current prompt
148    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, // Non-streaming request
157    };
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    // Send the request
167    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    // Calculate output tokens if we have a token counter
176    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    // Display token usage if available
183    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        // Show cost estimate if we have pricing info
192        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    // Try to get model metadata for context validation
230    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    // Create token counter
249    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    // Validate context size if we have both metadata and token counter
273    if let (Some(metadata), Some(ref counter)) = (&model_metadata, &token_counter) {
274        if let Some(context_limit) = metadata.context_length {
275            // Check if input exceeds context limit
276            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                // Truncate to fit within context limit
283                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    // Build messages for the request
314    let mut messages = Vec::new();
315
316    // Add system prompt if provided
317    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    // Add conversation history
329    for entry in &final_history {
330        messages.push(Message::user(entry.question.clone()));
331        messages.push(Message::assistant(entry.response.clone()));
332    }
333
334    // Add current prompt
335    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), // Enable streaming
344    };
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    // Send the streaming request
354    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    // If provider is configured for Google SA JWT (Vertex AI), use JWT Bearer flow
389    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 we have a valid cached token, use it (30s skew)
397    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        // Google OAuth 2.0 JWT Bearer flow
405        let token_url = provider
406            .token_url
407            .clone()
408            .unwrap_or_else(|| "https://oauth2.googleapis.com/token".to_string());
409
410        // Parse Service Account JSON from api_key
411        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        // Build JWT
432        #[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        // Exchange for access token
455        #[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    // Fallback: GitHub-style token endpoint using existing client helper
493    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
523// All providers now use OpenAIClient with template-based transformations
524pub type LLMClient = OpenAIClient;
525
526// Hardcoded conversion functions removed - now using template-based transformations
527
528pub 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    // Normalize chat_path placeholders: support both {model} and legacy {model_name}
547    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    // All providers now use OpenAIClient with template-based transformations
554    // Check if this needs OAuth authentication (Vertex AI)
555    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        // OAuth authentication flow (Vertex AI)
562        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        // Create custom headers with Authorization
573        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    // Regular authentication flow (API key or token URL)
592    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
614// New function to handle tool execution loop
615pub 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    // Create token counter for tracking usage
635    let token_counter = TokenCounter::new(model).ok();
636
637    // Add system prompt if provided
638    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    // Add conversation history
650    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    // Add current prompt
656    conversation_messages.push(Message::user(prompt.to_string()));
657    let max_iterations = 10; // Prevent infinite loops
658    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, // Non-streaming request for tool execution
678        };
679
680        // Make the API call
681        let response = client.chat_with_tools(&request).await?;
682
683        // Track token usage if we have a counter
684        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            // Check if the LLM made tool calls
701            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                    // Add the assistant's tool call message to conversation
710                    conversation_messages
711                        .push(Message::assistant_with_tool_calls(tool_calls.clone()));
712
713                    // Execute each tool call
714                    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                        // Find which MCP server has this function using daemon client
724                        let daemon_client = crate::mcp_daemon::DaemonClient::new()?;
725                        let mut tool_result = None;
726                        for server_name in mcp_server_names {
727                            // Parse arguments as JSON value instead of Vec<String>
728                            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                        // Add tool result to conversation
773                        conversation_messages
774                            .push(Message::tool_result(tool_call.id.clone(), result_content));
775                    }
776
777                    // Continue the loop to get the LLM's response to the tool results
778                    continue;
779                } else {
780                    // Empty tool_calls array - check if we have content (final answer)
781                    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                            // Track output tokens
791                            if let Some(ref counter) = token_counter {
792                                total_output_tokens += counter.count_tokens(content) as i32;
793                            }
794
795                            // Exit immediately when LLM provides content (final answer)
796                            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                // LLM provided a final answer without tool calls field
806                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                // Track output tokens
817                if let Some(ref counter) = token_counter {
818                    total_output_tokens += counter.count_tokens(content) as i32;
819                }
820
821                // Exit immediately when LLM provides content (final answer)
822                return Ok((
823                    content.clone(),
824                    Some(total_input_tokens),
825                    Some(total_output_tokens),
826                ));
827            } else {
828                // LLM provided neither tool calls nor content - this shouldn't happen
829                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
844// Helper function to format tool result for display
845fn 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    // Fallback to pretty-printed JSON
862    serde_json::to_string_pretty(result).unwrap_or_else(|_| "Error formatting result".to_string())
863}
864
865// Message-based versions of the chat functions for handling multimodal content
866
867pub 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    // Build final messages including system prompt if needed
885    let mut final_messages = Vec::new();
886
887    // Add system prompt if provided and not already in messages
888    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    // Add all provided messages
903    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    // For now, return None for token counts as we'd need to implement multimodal token counting
917    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    // Build final messages including system prompt if needed
938    let mut final_messages = Vec::new();
939
940    // Add system prompt if provided and not already in messages
941    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    // Add all provided messages
956    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    // Add system prompt if provided and not already in messages
989    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    // Add all provided messages
1004    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}