llm_link/
service.rs

1use crate::llm::{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        };
36
37        Ok(Self { client, model })
38    }
39
40    /// Chat with a specific model (non-streaming)
41    ///
42    /// If model is None, uses the default model from configuration.
43    #[allow(dead_code)]
44    pub async fn chat(
45        &self,
46        model: Option<&str>,
47        messages: Vec<llm_connector::types::Message>,
48        tools: Option<Vec<Tool>>,
49    ) -> Result<Response> {
50        let model = model.unwrap_or(&self.model);
51        self.client.chat(model, messages, tools).await
52    }
53
54    /// Chat with streaming (Ollama format)
55    ///
56    /// If model is None, uses the default model from configuration.
57    #[allow(dead_code)]
58    pub async fn chat_stream_ollama(
59        &self,
60        model: Option<&str>,
61        messages: Vec<llm_connector::types::Message>,
62        format: StreamFormat,
63    ) -> Result<UnboundedReceiverStream<String>> {
64        let model = model.unwrap_or(&self.model);
65        self.client.chat_stream_with_format(model, messages, format).await
66    }
67
68    /// Chat with streaming (OpenAI format)
69    ///
70    /// If model is None, uses the default model from configuration.
71    #[allow(dead_code)]
72    pub async fn chat_stream_openai(
73        &self,
74        model: Option<&str>,
75        messages: Vec<llm_connector::types::Message>,
76        tools: Option<Vec<Tool>>,
77        format: StreamFormat,
78    ) -> Result<UnboundedReceiverStream<String>> {
79        let model = model.unwrap_or(&self.model);
80        self.client.chat_stream_openai(model, messages, tools, format).await
81    }
82
83    /// List available models
84    pub async fn list_models(&self) -> Result<Vec<Model>> {
85        self.client.list_models().await
86    }
87
88    /// Validate if a model is available
89    #[allow(dead_code)]
90    pub async fn validate_model(&self, model: &str) -> Result<bool> {
91        let available_models = self.client.list_models().await?;
92        Ok(available_models.iter().any(|m| m.id == model))
93    }
94}