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