1use crate::language_models::{BaseChatModel, BaseLanguageModel, LLMResult};
25use crate::runnables::Runnable;
26use crate::RunnableConfig;
27use async_trait::async_trait;
28use futures_util::{Stream, StreamExt};
29use lc_schema::Message;
30use std::fmt::{self, Display, Formatter};
31use std::pin::Pin;
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::sync::{Arc, Mutex};
34use std::time::Instant;
35
36#[derive(Debug)]
41#[non_exhaustive]
42pub enum RouterError {
43 Empty,
45 AllFailed {
48 tried: usize,
50 last: Box<dyn std::error::Error + Send + Sync>,
52 },
53 Model {
55 model: String,
57 source: Box<dyn std::error::Error + Send + Sync>,
59 },
60}
61
62impl Display for RouterError {
63 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
64 match self {
65 RouterError::Empty => write!(f, "no models configured in router"),
66 RouterError::AllFailed { tried, last } => {
67 write!(f, "all {} models failed; last error: {}", tried, last)
68 }
69 RouterError::Model { model, source } => {
70 write!(f, "model '{}' error: {}", model, source)
71 }
72 }
73 }
74}
75
76impl std::error::Error for RouterError {
77 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
78 match self {
79 RouterError::AllFailed { last, .. } => Some(last.as_ref()),
80 RouterError::Model { source, .. } => Some(source.as_ref()),
81 RouterError::Empty => None,
82 }
83 }
84}
85
86pub enum RoutingStrategy {
91 Fallback,
94 RoundRobin,
96 LeastLatency,
99 LowestCost,
101 InputDirected(Arc<dyn Fn(&str) -> usize + Send + Sync>),
104}
105
106pub struct RouterLLM {
110 name: String,
111 slots: Vec<ModelSlot>,
112 strategy: RoutingStrategy,
113 counter: AtomicUsize,
115}
116
117impl RouterLLM {
118 pub fn new(strategy: RoutingStrategy) -> Self {
121 Self {
122 name: "router".to_string(),
123 slots: Vec::new(),
124 strategy,
125 counter: AtomicUsize::new(0),
126 }
127 }
128
129 pub fn with_name(mut self, name: impl Into<String>) -> Self {
131 self.name = name.into();
132 self
133 }
134
135 pub fn with_model<M>(mut self, model: M) -> Self
137 where
138 M: BaseChatModel + 'static,
139 M::Error: std::error::Error + Send + Sync + 'static,
140 {
141 self.slots
142 .push(ModelSlot::new(Box::new(ModelAdapter(model)), None));
143 self
144 }
145
146 pub fn with_cost<M>(mut self, model: M, cost: f64) -> Self
148 where
149 M: BaseChatModel + 'static,
150 M::Error: std::error::Error + Send + Sync + 'static,
151 {
152 self.slots
153 .push(ModelSlot::new(Box::new(ModelAdapter(model)), Some(cost)));
154 self
155 }
156
157 pub fn with_fallbacks<M>(primary: M, fallbacks: Vec<M>) -> Self
161 where
162 M: BaseChatModel + 'static,
163 M::Error: std::error::Error + Send + Sync + 'static,
164 {
165 let mut router = RouterLLM::new(RoutingStrategy::Fallback);
166 router = router.with_model(primary);
167 for fb in fallbacks {
168 router = router.with_model(fb);
169 }
170 router
171 }
172
173 pub fn len(&self) -> usize {
175 self.slots.len()
176 }
177
178 pub fn is_empty(&self) -> bool {
180 self.slots.is_empty()
181 }
182
183 fn candidate_order(&self, input: &str) -> Vec<usize> {
185 let n = self.slots.len();
186 match &self.strategy {
187 RoutingStrategy::Fallback => (0..n).collect(),
188 RoutingStrategy::RoundRobin => {
189 if n == 0 {
190 (0..n).collect()
191 } else {
192 let start = self.counter.fetch_add(1, Ordering::SeqCst) % n;
193 (0..n).map(|i| (start + i) % n).collect()
194 }
195 }
196 RoutingStrategy::LeastLatency => {
197 let mut idx: Vec<usize> = (0..n).collect();
198 idx.sort_by(|a, b| {
199 let la = {
201 let v = self.slots[*a].latency();
202 if v == 0.0 {
203 f64::MAX
204 } else {
205 v
206 }
207 };
208 let lb = {
209 let v = self.slots[*b].latency();
210 if v == 0.0 {
211 f64::MAX
212 } else {
213 v
214 }
215 };
216 la.partial_cmp(&lb).unwrap_or(std::cmp::Ordering::Equal)
217 });
218 idx
219 }
220 RoutingStrategy::LowestCost => {
221 let mut idx: Vec<usize> = (0..n).collect();
222 idx.sort_by(|a, b| {
223 let ca = self.slots[*a].cost.unwrap_or(f64::MAX);
224 let cb = self.slots[*b].cost.unwrap_or(f64::MAX);
225 ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
226 });
227 idx
228 }
229 RoutingStrategy::InputDirected(f) => {
230 let primary = f(input);
231 if primary < n {
232 let mut order = vec![primary];
233 order.extend((0..n).filter(|&i| i != primary));
234 order
235 } else {
236 (0..n).collect()
238 }
239 }
240 }
241 }
242
243 fn first_text(messages: &[Message]) -> &str {
244 messages.first().map(|m| m.content.as_str()).unwrap_or("")
245 }
246
247 async fn chat_routed(
248 &self,
249 messages: Vec<Message>,
250 config: Option<RunnableConfig>,
251 ) -> Result<LLMResult, RouterError> {
252 if self.slots.is_empty() {
253 return Err(RouterError::Empty);
254 }
255 let order = self.candidate_order(Self::first_text(&messages));
256 let mut last_err: Option<Box<dyn std::error::Error + Send + Sync>> = None;
258 for &idx in &order {
259 let slot = &self.slots[idx];
260 let start = Instant::now();
261 let res = slot.model.chat(messages.clone(), config.clone()).await;
262 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
263 slot.update_latency(elapsed_ms);
264 match res {
265 Ok(result) => return Ok(result),
266 Err(e) => last_err = Some(Box::new(e)),
267 }
268 }
269 Err(RouterError::AllFailed {
270 tried: order.len(),
271 last: last_err.unwrap_or_else(|| {
272 Box::new(std::io::Error::other(
273 "candidate order produced no attempts",
274 ))
275 }),
276 })
277 }
278
279 async fn stream_chat_routed(
280 &self,
281 messages: Vec<Message>,
282 config: Option<RunnableConfig>,
283 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, RouterError>> + Send>>, RouterError> {
284 if self.slots.is_empty() {
285 return Err(RouterError::Empty);
286 }
287 let order = self.candidate_order(Self::first_text(&messages));
288 let mut last_err: Option<Box<dyn std::error::Error + Send + Sync>> = None;
290 for &idx in &order {
291 let slot = &self.slots[idx];
292 let start = Instant::now();
293 match slot
294 .model
295 .stream_chat(messages.clone(), config.clone())
296 .await
297 {
298 Ok(s) => {
299 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
300 slot.update_latency(elapsed_ms);
301 return Ok(s);
302 }
303 Err(e) => {
304 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
305 slot.update_latency(elapsed_ms);
306 last_err = Some(Box::new(e));
307 }
308 }
309 }
310 Err(RouterError::AllFailed {
311 tried: order.len(),
312 last: last_err.unwrap_or_else(|| {
313 Box::new(std::io::Error::other(
314 "candidate order produced no attempts",
315 ))
316 }),
317 })
318 }
319}
320
321#[async_trait]
322impl Runnable<Vec<Message>, LLMResult> for RouterLLM {
323 type Error = RouterError;
324
325 async fn invoke(
326 &self,
327 input: Vec<Message>,
328 config: Option<RunnableConfig>,
329 ) -> Result<LLMResult, Self::Error> {
330 self.chat_routed(input, config).await
331 }
332}
333
334#[async_trait]
335impl BaseLanguageModel<Vec<Message>, LLMResult> for RouterLLM {
336 fn model_name(&self) -> &str {
337 &self.name
338 }
339
340 fn get_num_tokens(&self, text: &str) -> usize {
341 crate::token_counter::count_tokens(text).unwrap_or_else(|e| {
342 log::warn!("token counting failed, falling back to byte-length estimate: {e}");
344 text.len()
345 })
346 }
347
348 fn with_temperature(self, _temp: f32) -> Self {
349 self
355 }
356
357 fn with_max_tokens(self, _max: usize) -> Self {
358 self
361 }
362}
363
364#[async_trait]
365impl BaseChatModel for RouterLLM {
366 async fn chat(
367 &self,
368 messages: Vec<Message>,
369 config: Option<RunnableConfig>,
370 ) -> Result<LLMResult, Self::Error> {
371 self.chat_routed(messages, config).await
372 }
373
374 async fn stream_chat(
375 &self,
376 messages: Vec<Message>,
377 config: Option<RunnableConfig>,
378 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error> {
379 self.stream_chat_routed(messages, config).await
380 }
381}
382
383#[async_trait]
389trait RoutedModel: Send + Sync {
390 async fn chat(
391 &self,
392 messages: Vec<Message>,
393 config: Option<RunnableConfig>,
394 ) -> Result<LLMResult, RouterError>;
395 async fn stream_chat(
396 &self,
397 messages: Vec<Message>,
398 config: Option<RunnableConfig>,
399 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, RouterError>> + Send>>, RouterError>;
400}
401
402struct ModelAdapter<M: BaseChatModel>(M);
405
406#[async_trait]
407impl<M: BaseChatModel> RoutedModel for ModelAdapter<M>
408where
409 M::Error: std::error::Error + Send + Sync + 'static,
410{
411 async fn chat(
412 &self,
413 messages: Vec<Message>,
414 config: Option<RunnableConfig>,
415 ) -> Result<LLMResult, RouterError> {
416 let name = self.0.model_name().to_string();
417 self.0
418 .chat(messages, config)
419 .await
420 .map_err(|e| RouterError::Model {
421 model: name,
422 source: Box::new(e),
423 })
424 }
425
426 async fn stream_chat(
427 &self,
428 messages: Vec<Message>,
429 config: Option<RunnableConfig>,
430 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, RouterError>> + Send>>, RouterError> {
431 let name = self.0.model_name().to_string();
432 let inner = self
433 .0
434 .stream_chat(messages, config)
435 .await
436 .map_err(|e| RouterError::Model {
437 model: name.clone(),
438 source: Box::new(e),
439 })?;
440 let mapped = inner.map(move |item| {
441 item.map_err(|e| RouterError::Model {
442 model: name.clone(),
443 source: Box::new(e),
444 })
445 });
446 Ok(Box::pin(mapped))
447 }
448}
449
450struct ModelSlot {
452 model: Box<dyn RoutedModel>,
453 cost: Option<f64>,
454 latency_ms: Mutex<f64>,
456}
457
458impl ModelSlot {
459 fn new(model: Box<dyn RoutedModel>, cost: Option<f64>) -> Self {
460 Self {
461 model,
462 cost,
463 latency_ms: Mutex::new(0.0),
464 }
465 }
466
467 fn latency(&self) -> f64 {
468 *self.latency_ms.lock().unwrap_or_else(|e| e.into_inner())
469 }
470
471 fn update_latency(&self, ms: f64) {
472 let mut cur = self.latency_ms.lock().unwrap_or_else(|e| e.into_inner());
473 if *cur == 0.0 {
474 *cur = ms;
475 } else {
476 *cur = *cur * 0.7 + ms * 0.3;
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
488
489 fn slot_at(cost: Option<f64>, latency: f64) -> ModelSlot {
490 struct Noop;
494 #[async_trait]
495 impl RoutedModel for Noop {
496 async fn chat(
497 &self,
498 _m: Vec<Message>,
499 _c: Option<RunnableConfig>,
500 ) -> Result<LLMResult, RouterError> {
501 Err(RouterError::Empty)
502 }
503 async fn stream_chat(
504 &self,
505 _m: Vec<Message>,
506 _c: Option<RunnableConfig>,
507 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, RouterError>> + Send>>, RouterError>
508 {
509 Err(RouterError::Empty)
510 }
511 }
512 let s = ModelSlot::new(Box::new(Noop), cost);
513 *s.latency_ms.lock().unwrap_or_else(|e| e.into_inner()) = latency;
514 s
515 }
516
517 fn router_with(slots: Vec<ModelSlot>, strategy: RoutingStrategy) -> RouterLLM {
518 RouterLLM {
519 name: "router".to_string(),
520 slots,
521 strategy,
522 counter: AtomicUsize::new(0),
523 }
524 }
525
526 #[test]
527 fn candidate_order_fallback_is_registration_order() {
528 let r = router_with(
529 vec![slot_at(None, 0.0), slot_at(None, 0.0), slot_at(None, 0.0)],
530 RoutingStrategy::Fallback,
531 );
532 assert_eq!(r.candidate_order(""), vec![0, 1, 2]);
533 }
534
535 #[test]
536 fn candidate_order_lowest_cost_sorts_by_cost() {
537 let r = router_with(
538 vec![
539 slot_at(Some(10.0), 0.0), slot_at(Some(1.0), 0.0), ],
542 RoutingStrategy::LowestCost,
543 );
544 assert_eq!(r.candidate_order(""), vec![1, 0]);
545 }
546
547 #[test]
548 fn candidate_order_least_latency_sorts_by_latency() {
549 let r = router_with(
550 vec![
551 slot_at(None, 80.0), slot_at(None, 5.0), ],
554 RoutingStrategy::LeastLatency,
555 );
556 assert_eq!(r.candidate_order(""), vec![1, 0]);
557 }
558
559 #[test]
560 fn candidate_order_input_directed_puts_primary_first() {
561 let r = router_with(
562 vec![slot_at(None, 0.0), slot_at(None, 0.0)],
563 RoutingStrategy::InputDirected(Arc::new(|s| if s.contains("x") { 1 } else { 0 })),
564 );
565 assert_eq!(r.candidate_order("hello"), vec![0, 1]);
566 assert_eq!(r.candidate_order("x marks"), vec![1, 0]);
567 }
568
569 #[test]
570 fn candidate_order_input_directed_invalid_index_falls_back() {
571 let r = router_with(
572 vec![slot_at(None, 0.0), slot_at(None, 0.0)],
573 RoutingStrategy::InputDirected(Arc::new(|_| 99)),
574 );
575 assert_eq!(r.candidate_order("hi"), vec![0, 1]);
576 }
577
578 #[test]
579 fn update_latency_ema_blends_samples() {
580 let s = slot_at(None, 0.0);
581 s.update_latency(100.0);
582 assert_eq!(s.latency(), 100.0); s.update_latency(100.0);
584 assert_eq!(s.latency(), 100.0);
586 s.update_latency(40.0);
587 assert_eq!(s.latency(), 82.0);
589 }
590}