llm_link/llm/
chat.rs

1use super::Client;
2use crate::llm::types::{Response, Usage};
3use anyhow::{anyhow, Result};
4use llm_connector::types::ChatRequest;
5
6impl Client {
7    /// Send a non-streaming chat request to the LLM
8    #[allow(dead_code)]
9    pub async fn chat(
10        &self,
11        model: &str,
12        messages: Vec<llm_connector::types::Message>,
13        tools: Option<Vec<llm_connector::types::Tool>>,
14    ) -> Result<Response> {
15        // Messages are already in llm-connector format
16        let request = ChatRequest {
17            model: model.to_string(),
18            messages,
19            tools,
20            ..Default::default()
21        };
22
23        let response = self.llm_client.chat(&request).await
24            .map_err(|e| anyhow!("LLM connector error: {}", e))?;
25
26        // Debug: log the raw response
27        tracing::info!("📦 Raw LLM response: {:?}", response);
28        tracing::info!("📦 Raw LLM response choices: {}", response.choices.len());
29        if let Some(choice) = response.choices.get(0) {
30            tracing::info!("📦 Message content: '{}'", choice.message.content_as_text());
31            tracing::info!("📦 Message reasoning_content: {:?}", choice.message.reasoning_content);
32            tracing::info!("📦 Message reasoning: {:?}", choice.message.reasoning);
33        } else {
34            tracing::warn!("⚠️ No choices in response!");
35        }
36
37        // Extract content and usage information
38        let (prompt_tokens, completion_tokens, total_tokens) = response.get_usage_safe();
39
40        // Extract content and tool_calls from choices[0].message or response.content
41        let (content, tool_calls) = if let Some(choice) = response.choices.get(0) {
42            let msg = &choice.message;
43
44            // Extract content (could be in content, reasoning_content, reasoning, etc.)
45            let content = if msg.is_text_only() && !msg.content_as_text().is_empty() {
46                msg.content_as_text()
47            } else if let Some(reasoning) = &msg.reasoning_content {
48                reasoning.clone()
49            } else if let Some(reasoning) = &msg.reasoning {
50                reasoning.clone()
51            } else {
52                String::new()
53            };
54
55            // Extract tool_calls if present
56            let tool_calls = msg.tool_calls.as_ref()
57                .and_then(|tc| serde_json::to_value(tc).ok());
58
59            (content, tool_calls)
60        } else if !response.content.is_empty() {
61            // Fallback: some providers (like Aliyun in llm-connector 0.4.16)
62            // put content directly in response.content instead of choices
63            tracing::info!("📦 Using response.content: '{}'", response.content);
64            (response.content.clone(), None)
65        } else {
66            (String::new(), None)
67        };
68
69        Ok(Response {
70            content,
71            model: response.model,
72            usage: Usage {
73                prompt_tokens,
74                completion_tokens,
75                total_tokens,
76            },
77            tool_calls,
78        })
79    }
80}
81