reagent_rs/services/llm/
client.rs1use std::{pin::Pin, sync::Arc};
2
3use futures::Stream;
4
5use crate::{
6 services::llm::{
7 models::{
8 chat::{ChatRequest, ChatResponse, ChatStreamChunk},
9 embedding::{EmbeddingsRequest, EmbeddingsResponse},
10 errors::InferenceClientError,
11 },
12 SchemaSpec, StructuredOuputFormat,
13 },
14 ClientConfig,
15};
16
17use super::providers::{
18 anthropic::AnthropicClient, mistral::MistralClient, ollama::OllamaClient, openai::OpenAiClient,
19 openrouter::OpenRouterClient,
20};
21
22#[derive(Debug, Clone, Default)]
23pub enum Provider {
24 #[default]
25 Ollama,
26 OpenAi,
27 Mistral,
28 Anthropic,
29 OpenRouter,
30}
31
32#[derive(Debug, Clone)]
33enum ClientInner {
34 Ollama(OllamaClient),
35 OpenAi(OpenAiClient),
36 Mistral(MistralClient),
37 Anthropic(AnthropicClient),
38 OpenRouter(OpenRouterClient),
39}
40
41#[derive(Clone, Debug)]
42pub struct InferenceClient {
43 config: ClientConfig,
44 inner: Arc<ClientInner>,
45}
46
47impl InferenceClient {
48 pub fn get_config(&self) -> &ClientConfig {
49 &self.config
50 }
51
52 pub fn structured_output_format(
53 &self,
54 spec: &SchemaSpec,
55 ) -> Result<serde_json::Value, InferenceClientError> {
56 match self.get_config().provider {
57 Some(Provider::Ollama) => Ok(OllamaClient::format(spec)),
58 Some(Provider::OpenAi) => Ok(OpenAiClient::format(spec)),
59 Some(Provider::OpenRouter) => Ok(OpenRouterClient::format(spec)),
60 _ => Err(InferenceClientError::Unsupported(
61 "Structured outputs not yet supported for this provider".into(),
62 )),
63 }
64 }
65
66 pub async fn chat(&self, req: ChatRequest) -> Result<ChatResponse, InferenceClientError> {
67 match &*self.inner {
68 ClientInner::Ollama(c) => c.chat(req).await,
69 ClientInner::OpenAi(c) => c.chat(req).await,
70 ClientInner::Mistral(c) => c.chat(req).await,
71 ClientInner::Anthropic(c) => c.chat(req).await,
72 ClientInner::OpenRouter(c) => c.chat(req).await,
73 }
74 }
75
76 pub async fn chat_stream(
77 &self,
78 req: ChatRequest,
79 ) -> Result<
80 Pin<Box<dyn Stream<Item = Result<ChatStreamChunk, InferenceClientError>> + Send + 'static>>,
81 InferenceClientError,
82 > {
83 match &*self.inner {
84 ClientInner::Ollama(c) => c.chat_stream(req).await,
85 ClientInner::OpenAi(c) => c.chat_stream(req).await,
86 ClientInner::Mistral(c) => c.chat_stream(req).await,
87 ClientInner::Anthropic(c) => c.chat_stream(req).await,
88 ClientInner::OpenRouter(c) => c.chat_stream(req).await,
89 }
90 }
91
92 pub async fn embeddings(
93 &self,
94 req: EmbeddingsRequest,
95 ) -> Result<EmbeddingsResponse, InferenceClientError> {
96 match &*self.inner {
97 ClientInner::Ollama(c) => c.embeddings(req).await,
98 ClientInner::OpenAi(c) => c.embeddings(req).await,
99 ClientInner::Mistral(c) => c.embeddings(req).await,
100 ClientInner::Anthropic(c) => c.embeddings(req).await,
101 ClientInner::OpenRouter(c) => c.embeddings(req).await,
102 }
103 }
104}
105
106impl TryFrom<ClientConfig> for InferenceClient {
107 type Error = InferenceClientError;
108
109 fn try_from(cfg: ClientConfig) -> Result<Self, Self::Error> {
110 let config = cfg.clone();
111 let Some(provider) = cfg.provider.clone() else {
112 return Err(InferenceClientError::Config("Provider not defined".into()));
113 };
114 let inner = match provider {
115 Provider::Ollama => ClientInner::Ollama(OllamaClient::new(cfg)?),
116 Provider::OpenAi => ClientInner::OpenAi(OpenAiClient::new(cfg)?),
117 Provider::Mistral => ClientInner::Mistral(MistralClient::new(cfg)?),
118 Provider::Anthropic => ClientInner::Anthropic(AnthropicClient::new(cfg)?),
119 Provider::OpenRouter => ClientInner::OpenRouter(OpenRouterClient::new(cfg)?),
120 };
121 Ok(Self {
122 config,
123 inner: Arc::new(inner),
124 })
125 }
126}