llm_kernel/llm/
middleware.rs1use async_trait::async_trait;
21
22use crate::error::{KernelError, Result};
23use crate::llm::client::LLMClient;
24use crate::llm::types::{LLMRequest, LLMResponse, LLMStream};
25
26#[async_trait]
34pub trait LLMClientMiddleware: Send + Sync {
35 async fn on_request(&self, _request: &LLMRequest) {}
39
40 async fn on_response(&self, _request: &LLMRequest, _response: &LLMResponse) {}
44
45 async fn on_error(&self, _request: &LLMRequest, _error: &KernelError) {}
49}
50
51pub struct NoopMiddleware;
53
54#[async_trait]
55impl LLMClientMiddleware for NoopMiddleware {
56 }
58
59pub struct MiddlewareClient<C, M> {
64 inner: C,
65 middleware: M,
66}
67
68impl<C, M> MiddlewareClient<C, M> {
69 pub fn new(inner: C, middleware: M) -> Self {
71 Self { inner, middleware }
72 }
73
74 pub fn inner(&self) -> &C {
76 &self.inner
77 }
78}
79
80#[async_trait]
81impl<C: LLMClient, M: LLMClientMiddleware> LLMClient for MiddlewareClient<C, M> {
82 async fn complete(&self, request: LLMRequest) -> Result<LLMResponse> {
83 self.middleware.on_request(&request).await;
84 match self.inner.complete(request.clone()).await {
85 Ok(response) => {
86 self.middleware.on_response(&request, &response).await;
87 Ok(response)
88 }
89 Err(err) => {
90 self.middleware.on_error(&request, &err).await;
91 Err(err)
92 }
93 }
94 }
95
96 fn model_name(&self) -> &str {
97 self.inner.model_name()
98 }
99
100 async fn stream_complete(&self, request: LLMRequest) -> Result<LLMStream> {
101 self.inner.stream_complete(request).await
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use std::sync::{Arc, Mutex};
111
112 #[derive(Default)]
114 struct RecordingMiddleware {
115 on_request_called: Arc<Mutex<bool>>,
116 on_response_called: Arc<Mutex<bool>>,
117 on_error_called: Arc<Mutex<bool>>,
118 }
119
120 #[async_trait]
121 impl LLMClientMiddleware for RecordingMiddleware {
122 async fn on_request(&self, _request: &LLMRequest) {
123 *self.on_request_called.lock().unwrap() = true;
124 }
125
126 async fn on_response(&self, _request: &LLMRequest, _response: &LLMResponse) {
127 *self.on_response_called.lock().unwrap() = true;
128 }
129
130 async fn on_error(&self, _request: &LLMRequest, _error: &KernelError) {
131 *self.on_error_called.lock().unwrap() = true;
132 }
133 }
134
135 struct MockClient {
137 response: std::sync::Mutex<Option<Result<LLMResponse>>>,
138 }
139
140 impl MockClient {
141 fn ok() -> Self {
142 Self {
143 response: std::sync::Mutex::new(Some(Ok(LLMResponse {
144 content: "hello".into(),
145 model: "mock".into(),
146 ..Default::default()
147 }))),
148 }
149 }
150
151 fn err() -> Self {
152 Self {
153 response: std::sync::Mutex::new(Some(Err(KernelError::LlmApi("fail".into())))),
154 }
155 }
156 }
157
158 #[async_trait]
159 impl LLMClient for MockClient {
160 async fn complete(&self, _request: LLMRequest) -> Result<LLMResponse> {
161 self.response.lock().unwrap().take().unwrap()
162 }
163
164 fn model_name(&self) -> &str {
165 "mock"
166 }
167
168 async fn stream_complete(&self, _request: LLMRequest) -> Result<LLMStream> {
169 unimplemented!()
170 }
171 }
172
173 #[tokio::test]
174 async fn middleware_calls_on_request_and_on_response_on_success() {
175 let mid = RecordingMiddleware::default();
176 let req_called = mid.on_request_called.clone();
177 let res_called = mid.on_response_called.clone();
178
179 let client = MiddlewareClient::new(MockClient::ok(), mid);
180 let result = client.complete(LLMRequest::builder().build()).await;
181
182 assert!(result.is_ok());
183 assert!(*req_called.lock().unwrap());
184 assert!(*res_called.lock().unwrap());
185 }
186
187 #[tokio::test]
188 async fn middleware_calls_on_error_on_failure() {
189 let mid = RecordingMiddleware::default();
190 let err_called = mid.on_error_called.clone();
191
192 let client = MiddlewareClient::new(MockClient::err(), mid);
193 let result = client.complete(LLMRequest::builder().build()).await;
194
195 assert!(result.is_err());
196 assert!(*err_called.lock().unwrap());
197 }
198
199 #[tokio::test]
200 async fn middleware_delegates_model_name() {
201 let client = MiddlewareClient::new(MockClient::ok(), NoopMiddleware);
202 assert_eq!(client.model_name(), "mock");
203 }
204
205 #[tokio::test]
206 async fn noop_middleware_compiles_and_works() {
207 let client = MiddlewareClient::new(MockClient::ok(), NoopMiddleware);
208 let result = client.complete(LLMRequest::builder().build()).await;
209 assert!(result.is_ok());
210 }
211}