llm_link/
service.rs

1use crate::normalizer::{Client, Model, Response};
2use crate::settings::LlmBackendSettings;
3use anyhow::Result;
4use llm_connector::types::Tool;
5use llm_connector::StreamFormat;
6use tokio_stream::wrappers::UnboundedReceiverStream;
7
8/// Service layer - Business logic for LLM operations
9///
10/// This layer sits between handlers (HTTP) and client (LLM communication).
11/// It handles:
12/// - Model selection and validation
13/// - Delegating to the appropriate client methods
14/// - Business-level error handling
15pub struct Service {
16    client: Client,
17    #[allow(dead_code)]
18    model: String,
19}
20
21impl Service {
22    /// Create a new service with the specified backend configuration
23    pub fn new(config: &LlmBackendSettings) -> Result<Self> {
24        let client = Client::new(config)?;
25        let model = match config {
26            LlmBackendSettings::OpenAI { model, .. } => model.clone(),
27            LlmBackendSettings::Anthropic { model, .. } => model.clone(),
28            LlmBackendSettings::Ollama { model, .. } => model.clone(),
29            LlmBackendSettings::Aliyun { model, .. } => model.clone(),
30            LlmBackendSettings::Zhipu { model, .. } => model.clone(),
31            LlmBackendSettings::Volcengine { model, .. } => model.clone(),
32            LlmBackendSettings::Tencent { model, .. } => model.clone(),
33            LlmBackendSettings::Longcat { model, .. } => model.clone(),
34            LlmBackendSettings::Moonshot { model, .. } => model.clone(),
35            LlmBackendSettings::Minimax { model, .. } => model.clone(),
36        };
37
38        Ok(Self { client, model })
39    }
40
41    /// Chat with a specific model (non-streaming)
42    ///
43    /// If model is None, uses the default model from configuration.
44    #[allow(dead_code)]
45    pub async fn chat(
46        &self,
47        model: Option<&str>,
48        messages: Vec<llm_connector::types::Message>,
49        tools: Option<Vec<Tool>>,
50    ) -> Result<Response> {
51        let requested = model.unwrap_or(&self.model);
52        let backend_model = self
53            .client
54            .resolve_model(requested, &self.model);
55        self.client.chat(&backend_model, messages, tools).await
56    }
57
58    /// Chat with streaming (Ollama format)
59    ///
60    /// If model is None, uses the default model from configuration.
61    #[allow(dead_code)]
62    pub async fn chat_stream_ollama(
63        &self,
64        model: Option<&str>,
65        messages: Vec<llm_connector::types::Message>,
66        format: StreamFormat,
67    ) -> Result<UnboundedReceiverStream<String>> {
68        let requested = model.unwrap_or(&self.model);
69        let backend_model = self
70            .client
71            .resolve_model(requested, &self.model);
72        self.client
73            .chat_stream_with_format(&backend_model, messages, format)
74            .await
75    }
76
77    /// Chat with streaming (Ollama format) with tools support
78    ///
79    /// If model is None, uses the default model from configuration.
80    #[allow(dead_code)]
81    pub async fn chat_stream_ollama_with_tools(
82        &self,
83        model: Option<&str>,
84        messages: Vec<llm_connector::types::Message>,
85        tools: Option<Vec<llm_connector::types::Tool>>,
86        format: StreamFormat,
87    ) -> Result<UnboundedReceiverStream<String>> {
88        let requested = model.unwrap_or(&self.model);
89        let backend_model = self
90            .client
91            .resolve_model(requested, &self.model);
92        self.client
93            .chat_stream_with_format_and_tools(&backend_model, messages, tools, format)
94            .await
95    }
96
97    /// Chat with streaming (OpenAI format)
98    ///
99    /// If model is None, uses the default model from configuration.
100    #[allow(dead_code)]
101    pub async fn chat_stream_openai(
102        &self,
103        model: Option<&str>,
104        messages: Vec<llm_connector::types::Message>,
105        tools: Option<Vec<Tool>>,
106        format: StreamFormat,
107    ) -> Result<UnboundedReceiverStream<String>> {
108        let requested = model.unwrap_or(&self.model);
109        let backend_model = self
110            .client
111            .resolve_model(requested, &self.model);
112        self.client
113            .chat_stream_openai(&backend_model, messages, tools, format)
114            .await
115    }
116
117    /// List available models
118    pub async fn list_models(&self) -> Result<Vec<Model>> {
119        self.client.list_models().await
120    }
121
122    /// Validate if a model is available
123    #[allow(dead_code)]
124    pub async fn validate_model(&self, model: &str) -> Result<bool> {
125        let available_models = self.client.list_models().await?;
126        Ok(available_models.iter().any(|m| m.id == model))
127    }
128}