lc_core/language_models/chat.rs
1// src/core/language_models/chat.rs
2//! Chat model base trait.
3
4use super::BaseLanguageModel;
5use crate::tools::ToolDefinition;
6use crate::RunnableConfig;
7use async_trait::async_trait;
8use futures_util::Stream;
9use lc_schema::Message;
10use lc_shared::tools::ToolCall;
11use serde::{Deserialize, Serialize};
12use std::pin::Pin;
13
14/// LLM result containing response content and metadata.
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct LLMResult {
17 /// The generated response content.
18 #[serde(default)]
19 pub content: String,
20 /// The model identifier that produced the result.
21 #[serde(default)]
22 pub model: String,
23 /// Token usage statistics, if reported.
24 #[serde(default)]
25 pub token_usage: Option<TokenUsage>,
26 /// Tool calls requested by the model, if any.
27 #[serde(default)]
28 pub tool_calls: Option<Vec<ToolCall>>,
29 /// Model reasoning/thinking content, if present.
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub thinking_content: Option<String>,
32}
33
34/// Token usage statistics.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TokenUsage {
37 /// Input token count.
38 pub prompt_tokens: usize,
39
40 /// Output token count.
41 pub completion_tokens: usize,
42
43 /// Total token count.
44 pub total_tokens: usize,
45}
46
47/// Base trait for chat models.
48///
49/// Extends BaseLanguageModel for chat scenarios.
50/// Accepts message list as input, returns AI message.
51#[async_trait]
52pub trait BaseChatModel: BaseLanguageModel<Vec<Message>, LLMResult> {
53 /// Chat with the model.
54 ///
55 /// # Arguments
56 /// * `messages` - Message list.
57 /// * `config` - Optional configuration.
58 ///
59 /// # Returns
60 /// LLM result.
61 async fn chat(
62 &self,
63 messages: Vec<Message>,
64 config: Option<RunnableConfig>,
65 ) -> Result<LLMResult, Self::Error>;
66
67 /// Stream chat with the model.
68 ///
69 /// # Arguments
70 /// * `messages` - Message list.
71 /// * `config` - Optional configuration.
72 ///
73 /// # Returns
74 /// Stream of output chunks.
75 async fn stream_chat(
76 &self,
77 messages: Vec<Message>,
78 config: Option<RunnableConfig>,
79 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>;
80
81 /// Chat with system prompt.
82 ///
83 /// # Arguments
84 /// * `system` - System prompt.
85 /// * `messages` - Message list.
86 ///
87 /// # Returns
88 /// LLM result.
89 async fn chat_with_system(
90 &self,
91 system: String,
92 messages: Vec<Message>,
93 ) -> Result<LLMResult, Self::Error> {
94 let full_messages = vec![Message::system(system)]
95 .into_iter()
96 .chain(messages)
97 .collect();
98
99 self.chat(full_messages, None).await
100 }
101
102 /// Bind tool definitions for function calling.
103 ///
104 /// Returns `Some(model)` with the tools attached when the provider
105 /// supports tool calling; returns `None` when it does not. **The default
106 /// returns `None`, signalling a hard capability limit** — callers MUST
107 /// treat `None` as "this model cannot call tools" and branch accordingly
108 /// (e.g. fall back to text-only prompting). Providers that support
109 /// function calling (OpenAI, Ollama) override this.
110 ///
111 /// This is an explicit result, not a silent degrade: `None` is the honest
112 /// answer that tool-calling is unavailable on this model.
113 fn bind_tools(
114 &self,
115 _tools: Vec<ToolDefinition>,
116 ) -> Option<Box<dyn BaseChatModel<Error = Self::Error> + Send + Sync>> {
117 None
118 }
119}