lib.rs 22.3 KB
Newer Older
1
use pyo3::prelude::*;
2
pub mod config;
3
pub mod logging;
4
use std::collections::HashMap;
5

6
pub mod core;
7
pub mod data_connector;
8
#[cfg(feature = "grpc-client")]
9
pub mod grpc_client;
10
pub mod mcp;
11
pub mod metrics;
12
pub mod middleware;
13
pub mod policies;
14
pub mod protocols;
15
pub mod reasoning_parser;
16
pub mod routers;
17
pub mod server;
18
pub mod service_discovery;
19
pub mod tokenizer;
20
pub mod tool_parser;
21
pub mod tree;
22
use crate::metrics::PrometheusConfig;
23

24
#[pyclass(eq)]
25
#[derive(Clone, PartialEq, Debug)]
26
27
28
pub enum PolicyType {
    Random,
    RoundRobin,
29
    CacheAware,
30
    PowerOfTwo,
31
32
}

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#[pyclass(eq)]
#[derive(Clone, PartialEq, Debug)]
pub enum BackendType {
    Sglang,
    Openai,
}

#[pyclass(eq)]
#[derive(Clone, PartialEq, Debug)]
pub enum HistoryBackendType {
    Memory,
    None,
    Oracle,
}

#[pyclass]
#[derive(Clone, PartialEq)]
pub struct PyOracleConfig {
    #[pyo3(get, set)]
    pub wallet_path: Option<String>,
    #[pyo3(get, set)]
    pub connect_descriptor: Option<String>,
    #[pyo3(get, set)]
    pub username: Option<String>,
    #[pyo3(get, set)]
    pub password: Option<String>,
    #[pyo3(get, set)]
    pub pool_min: usize,
    #[pyo3(get, set)]
    pub pool_max: usize,
    #[pyo3(get, set)]
    pub pool_timeout_secs: u64,
}

impl std::fmt::Debug for PyOracleConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("PyOracleConfig")
            .field("wallet_path", &self.wallet_path)
            .field("connect_descriptor", &"<redacted>")
            .field("username", &self.username)
            .field("password", &"<redacted>")
            .field("pool_min", &self.pool_min)
            .field("pool_max", &self.pool_max)
            .field("pool_timeout_secs", &self.pool_timeout_secs)
            .finish()
    }
}

#[pymethods]
impl PyOracleConfig {
    #[new]
    #[pyo3(signature = (
        password = None,
        username = None,
        connect_descriptor = None,
        wallet_path = None,
        pool_min = 1,
        pool_max = 16,
        pool_timeout_secs = 30,
    ))]
    fn new(
        password: Option<String>,
        username: Option<String>,
        connect_descriptor: Option<String>,
        wallet_path: Option<String>,
        pool_min: usize,
        pool_max: usize,
        pool_timeout_secs: u64,
    ) -> PyResult<Self> {
        if pool_min == 0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "pool_min must be at least 1",
            ));
        }
        if pool_max < pool_min {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "pool_max must be >= pool_min",
            ));
        }

        Ok(PyOracleConfig {
            wallet_path,
            connect_descriptor,
            username,
            password,
            pool_min,
            pool_max,
            pool_timeout_secs,
        })
    }
}

impl PyOracleConfig {
    fn to_config_oracle(&self) -> config::OracleConfig {
        // Simple conversion - validation happens later in validate_oracle()
        config::OracleConfig {
            wallet_path: self.wallet_path.clone(),
            connect_descriptor: self.connect_descriptor.clone().unwrap_or_default(),
            username: self.username.clone().unwrap_or_default(),
            password: self.password.clone().unwrap_or_default(),
            pool_min: self.pool_min,
            pool_max: self.pool_max,
            pool_timeout_secs: self.pool_timeout_secs,
        }
    }
}

140
#[pyclass]
141
#[derive(Debug, Clone, PartialEq)]
142
143
144
145
struct Router {
    host: String,
    port: u16,
    worker_urls: Vec<String>,
146
    policy: PolicyType,
147
    worker_startup_timeout_secs: u64,
148
    worker_startup_check_interval: u64,
149
    cache_threshold: f32,
150
151
    balance_abs_threshold: usize,
    balance_rel_threshold: f32,
152
153
    eviction_interval_secs: u64,
    max_tree_size: usize,
154
    max_payload_size: usize,
155
156
    dp_aware: bool,
    api_key: Option<String>,
157
    log_dir: Option<String>,
158
    log_level: Option<String>,
159
160
161
162
    service_discovery: bool,
    selector: HashMap<String, String>,
    service_discovery_port: u16,
    service_discovery_namespace: Option<String>,
163
164
165
    prefill_selector: HashMap<String, String>,
    decode_selector: HashMap<String, String>,
    bootstrap_port_annotation: String,
166
167
    prometheus_port: Option<u16>,
    prometheus_host: Option<String>,
168
    request_timeout_secs: u64,
169
    request_id_headers: Option<Vec<String>>,
170
    pd_disaggregation: bool,
171
172
    prefill_urls: Option<Vec<(String, Option<u16>)>>,
    decode_urls: Option<Vec<String>>,
173
174
    prefill_policy: Option<PolicyType>,
    decode_policy: Option<PolicyType>,
175
    max_concurrent_requests: i32,
176
    cors_allowed_origins: Vec<String>,
177
178
179
180
181
182
183
184
185
186
187
    retry_max_retries: u32,
    retry_initial_backoff_ms: u64,
    retry_max_backoff_ms: u64,
    retry_backoff_multiplier: f32,
    retry_jitter_factor: f32,
    disable_retries: bool,
    cb_failure_threshold: u32,
    cb_success_threshold: u32,
    cb_timeout_duration_secs: u64,
    cb_window_duration_secs: u64,
    disable_circuit_breaker: bool,
188
189
190
191
192
    health_failure_threshold: u32,
    health_success_threshold: u32,
    health_check_timeout_secs: u64,
    health_check_interval_secs: u64,
    health_check_endpoint: String,
193
    enable_igw: bool,
194
195
    queue_size: usize,
    queue_timeout_secs: u64,
196
    rate_limit_tokens_per_second: Option<i32>,
197
    connection_mode: core::ConnectionMode,
198
199
    model_path: Option<String>,
    tokenizer_path: Option<String>,
200
    chat_template: Option<String>,
201
202
203
204
    tokenizer_cache_enable_l0: bool,
    tokenizer_cache_l0_max_entries: usize,
    tokenizer_cache_enable_l1: bool,
    tokenizer_cache_l1_max_memory: usize,
205
206
    reasoning_parser: Option<String>,
    tool_call_parser: Option<String>,
207
208
209
    backend: BackendType,
    history_backend: HistoryBackendType,
    oracle_config: Option<PyOracleConfig>,
210
211
}

212
impl Router {
213
    /// Determine connection mode from worker URLs
214
    fn determine_connection_mode(worker_urls: &[String]) -> core::ConnectionMode {
215
216
        for url in worker_urls {
            if url.starts_with("grpc://") || url.starts_with("grpcs://") {
217
                return core::ConnectionMode::Grpc { port: None };
218
219
            }
        }
220
        core::ConnectionMode::Http
221
222
    }

223
224
225
226
227
    pub fn to_router_config(&self) -> config::ConfigResult<config::RouterConfig> {
        use config::{
            DiscoveryConfig, MetricsConfig, PolicyConfig as ConfigPolicyConfig, RoutingMode,
        };

228
229
230
231
232
233
234
235
236
237
238
239
        let convert_policy = |policy: &PolicyType| -> ConfigPolicyConfig {
            match policy {
                PolicyType::Random => ConfigPolicyConfig::Random,
                PolicyType::RoundRobin => ConfigPolicyConfig::RoundRobin,
                PolicyType::CacheAware => ConfigPolicyConfig::CacheAware {
                    cache_threshold: self.cache_threshold,
                    balance_abs_threshold: self.balance_abs_threshold,
                    balance_rel_threshold: self.balance_rel_threshold,
                    eviction_interval_secs: self.eviction_interval_secs,
                    max_tree_size: self.max_tree_size,
                },
                PolicyType::PowerOfTwo => ConfigPolicyConfig::PowerOfTwo {
240
                    load_check_interval_secs: 5,
241
242
243
244
                },
            }
        };

245
246
247
248
        let mode = if self.enable_igw {
            RoutingMode::Regular {
                worker_urls: vec![],
            }
249
250
251
252
        } else if matches!(self.backend, BackendType::Openai) {
            RoutingMode::OpenAI {
                worker_urls: self.worker_urls.clone(),
            }
253
        } else if self.pd_disaggregation {
254
255
256
            RoutingMode::PrefillDecode {
                prefill_urls: self.prefill_urls.clone().unwrap_or_default(),
                decode_urls: self.decode_urls.clone().unwrap_or_default(),
257
258
                prefill_policy: self.prefill_policy.as_ref().map(convert_policy),
                decode_policy: self.decode_policy.as_ref().map(convert_policy),
259
260
261
262
263
264
265
            }
        } else {
            RoutingMode::Regular {
                worker_urls: self.worker_urls.clone(),
            }
        };

266
        let policy = convert_policy(&self.policy);
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290

        let discovery = if self.service_discovery {
            Some(DiscoveryConfig {
                enabled: true,
                namespace: self.service_discovery_namespace.clone(),
                port: self.service_discovery_port,
                check_interval_secs: 60,
                selector: self.selector.clone(),
                prefill_selector: self.prefill_selector.clone(),
                decode_selector: self.decode_selector.clone(),
                bootstrap_port_annotation: self.bootstrap_port_annotation.clone(),
            })
        } else {
            None
        };

        let metrics = match (self.prometheus_port, self.prometheus_host.as_ref()) {
            (Some(port), Some(host)) => Some(MetricsConfig {
                port,
                host: host.clone(),
            }),
            _ => None,
        };

291
292
293
294
295
296
297
298
299
300
301
302
303
304
        let history_backend = match self.history_backend {
            HistoryBackendType::Memory => config::HistoryBackend::Memory,
            HistoryBackendType::None => config::HistoryBackend::None,
            HistoryBackendType::Oracle => config::HistoryBackend::Oracle,
        };

        let oracle = if matches!(self.history_backend, HistoryBackendType::Oracle) {
            self.oracle_config
                .as_ref()
                .map(|cfg| cfg.to_config_oracle())
        } else {
            None
        };

305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
        let builder = config::RouterConfig::builder()
            .mode(mode)
            .policy(policy)
            .host(&self.host)
            .port(self.port)
            .connection_mode(self.connection_mode.clone())
            .max_payload_size(self.max_payload_size)
            .request_timeout_secs(self.request_timeout_secs)
            .worker_startup_timeout_secs(self.worker_startup_timeout_secs)
            .worker_startup_check_interval_secs(self.worker_startup_check_interval)
            .max_concurrent_requests(self.max_concurrent_requests)
            .queue_size(self.queue_size)
            .queue_timeout_secs(self.queue_timeout_secs)
            .cors_allowed_origins(self.cors_allowed_origins.clone())
            .retry_config(config::RetryConfig {
320
321
322
323
324
                max_retries: self.retry_max_retries,
                initial_backoff_ms: self.retry_initial_backoff_ms,
                max_backoff_ms: self.retry_max_backoff_ms,
                backoff_multiplier: self.retry_backoff_multiplier,
                jitter_factor: self.retry_jitter_factor,
325
326
            })
            .circuit_breaker_config(config::CircuitBreakerConfig {
327
328
329
330
                failure_threshold: self.cb_failure_threshold,
                success_threshold: self.cb_success_threshold,
                timeout_duration_secs: self.cb_timeout_duration_secs,
                window_duration_secs: self.cb_window_duration_secs,
331
332
            })
            .health_check_config(config::HealthCheckConfig {
333
334
335
336
337
                failure_threshold: self.health_failure_threshold,
                success_threshold: self.health_success_threshold,
                timeout_secs: self.health_check_timeout_secs,
                check_interval_secs: self.health_check_interval_secs,
                endpoint: self.health_check_endpoint.clone(),
338
339
            })
            .tokenizer_cache(config::TokenizerCacheConfig {
340
341
342
343
                enable_l0: self.tokenizer_cache_enable_l0,
                l0_max_entries: self.tokenizer_cache_l0_max_entries,
                enable_l1: self.tokenizer_cache_enable_l1,
                l1_max_memory: self.tokenizer_cache_l1_max_memory,
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
            })
            .history_backend(history_backend)
            .maybe_api_key(self.api_key.as_ref())
            .maybe_discovery(discovery)
            .maybe_metrics(metrics)
            .maybe_log_dir(self.log_dir.as_ref())
            .maybe_log_level(self.log_level.as_ref())
            .maybe_request_id_headers(self.request_id_headers.clone())
            .maybe_rate_limit_tokens_per_second(self.rate_limit_tokens_per_second)
            .maybe_model_path(self.model_path.as_ref())
            .maybe_tokenizer_path(self.tokenizer_path.as_ref())
            .maybe_chat_template(self.chat_template.as_ref())
            .maybe_oracle(oracle)
            .maybe_reasoning_parser(self.reasoning_parser.as_ref())
            .maybe_tool_call_parser(self.tool_call_parser.as_ref())
            .dp_aware(self.dp_aware)
            .retries(!self.disable_retries)
            .circuit_breaker(!self.disable_circuit_breaker)
            .igw(self.enable_igw);

        builder.build()
365
366
367
    }
}

368
369
370
#[pymethods]
impl Router {
    #[new]
371
372
373
    #[pyo3(signature = (
        worker_urls,
        policy = PolicyType::RoundRobin,
374
        host = String::from("0.0.0.0"),
375
        port = 3001,
376
377
378
379
380
381
382
        worker_startup_timeout_secs = 600,
        worker_startup_check_interval = 30,
        cache_threshold = 0.3,
        balance_abs_threshold = 64,
        balance_rel_threshold = 1.5,
        eviction_interval_secs = 120,
        max_tree_size = 2usize.pow(26),
383
        max_payload_size = 512 * 1024 * 1024,
384
385
        dp_aware = false,
        api_key = None,
386
        log_dir = None,
387
        log_level = None,
388
389
390
        service_discovery = false,
        selector = HashMap::new(),
        service_discovery_port = 80,
391
        service_discovery_namespace = None,
392
393
394
        prefill_selector = HashMap::new(),
        decode_selector = HashMap::new(),
        bootstrap_port_annotation = String::from("sglang.ai/bootstrap-port"),
395
        prometheus_port = None,
396
        prometheus_host = None,
397
398
399
        request_timeout_secs = 1800,
        request_id_headers = None,
        pd_disaggregation = false,
400
        prefill_urls = None,
401
402
        decode_urls = None,
        prefill_policy = None,
403
        decode_policy = None,
404
        max_concurrent_requests = -1,
405
        cors_allowed_origins = vec![],
406
407
408
409
410
        retry_max_retries = 5,
        retry_initial_backoff_ms = 50,
        retry_max_backoff_ms = 30_000,
        retry_backoff_multiplier = 1.5,
        retry_jitter_factor = 0.2,
411
        disable_retries = false,
412
413
414
415
        cb_failure_threshold = 10,
        cb_success_threshold = 3,
        cb_timeout_duration_secs = 60,
        cb_window_duration_secs = 120,
416
        disable_circuit_breaker = false,
417
418
419
420
421
        health_failure_threshold = 3,
        health_success_threshold = 2,
        health_check_timeout_secs = 5,
        health_check_interval_secs = 60,
        health_check_endpoint = String::from("/health"),
422
        enable_igw = false,
423
424
425
        queue_size = 100,
        queue_timeout_secs = 60,
        rate_limit_tokens_per_second = None,
426
427
        model_path = None,
        tokenizer_path = None,
428
        chat_template = None,
429
430
431
432
        tokenizer_cache_enable_l0 = false,
        tokenizer_cache_l0_max_entries = 10000,
        tokenizer_cache_enable_l1 = false,
        tokenizer_cache_l1_max_memory = 52428800,
433
434
        reasoning_parser = None,
        tool_call_parser = None,
435
436
437
        backend = BackendType::Sglang,
        history_backend = HistoryBackendType::Memory,
        oracle_config = None,
438
    ))]
439
    #[allow(clippy::too_many_arguments)]
440
441
442
443
444
    fn new(
        worker_urls: Vec<String>,
        policy: PolicyType,
        host: String,
        port: u16,
445
        worker_startup_timeout_secs: u64,
446
        worker_startup_check_interval: u64,
447
        cache_threshold: f32,
448
449
        balance_abs_threshold: usize,
        balance_rel_threshold: f32,
450
451
        eviction_interval_secs: u64,
        max_tree_size: usize,
452
        max_payload_size: usize,
453
454
        dp_aware: bool,
        api_key: Option<String>,
455
        log_dir: Option<String>,
456
        log_level: Option<String>,
457
458
459
460
        service_discovery: bool,
        selector: HashMap<String, String>,
        service_discovery_port: u16,
        service_discovery_namespace: Option<String>,
461
462
463
        prefill_selector: HashMap<String, String>,
        decode_selector: HashMap<String, String>,
        bootstrap_port_annotation: String,
464
465
        prometheus_port: Option<u16>,
        prometheus_host: Option<String>,
466
        request_timeout_secs: u64,
467
        request_id_headers: Option<Vec<String>>,
468
        pd_disaggregation: bool,
469
470
        prefill_urls: Option<Vec<(String, Option<u16>)>>,
        decode_urls: Option<Vec<String>>,
471
472
        prefill_policy: Option<PolicyType>,
        decode_policy: Option<PolicyType>,
473
        max_concurrent_requests: i32,
474
        cors_allowed_origins: Vec<String>,
475
476
477
478
479
480
481
482
483
484
485
        retry_max_retries: u32,
        retry_initial_backoff_ms: u64,
        retry_max_backoff_ms: u64,
        retry_backoff_multiplier: f32,
        retry_jitter_factor: f32,
        disable_retries: bool,
        cb_failure_threshold: u32,
        cb_success_threshold: u32,
        cb_timeout_duration_secs: u64,
        cb_window_duration_secs: u64,
        disable_circuit_breaker: bool,
486
487
488
489
490
        health_failure_threshold: u32,
        health_success_threshold: u32,
        health_check_timeout_secs: u64,
        health_check_interval_secs: u64,
        health_check_endpoint: String,
491
        enable_igw: bool,
492
493
        queue_size: usize,
        queue_timeout_secs: u64,
494
        rate_limit_tokens_per_second: Option<i32>,
495
496
        model_path: Option<String>,
        tokenizer_path: Option<String>,
497
        chat_template: Option<String>,
498
499
500
501
        tokenizer_cache_enable_l0: bool,
        tokenizer_cache_l0_max_entries: usize,
        tokenizer_cache_enable_l1: bool,
        tokenizer_cache_l1_max_memory: usize,
502
503
        reasoning_parser: Option<String>,
        tool_call_parser: Option<String>,
504
505
506
        backend: BackendType,
        history_backend: HistoryBackendType,
        oracle_config: Option<PyOracleConfig>,
507
    ) -> PyResult<Self> {
508
509
510
511
512
513
514
515
516
517
518
519
520
521
        let mut all_urls = worker_urls.clone();

        if let Some(ref prefill_urls) = prefill_urls {
            for (url, _) in prefill_urls {
                all_urls.push(url.clone());
            }
        }

        if let Some(ref decode_urls) = decode_urls {
            all_urls.extend(decode_urls.clone());
        }

        let connection_mode = Self::determine_connection_mode(&all_urls);

522
        Ok(Router {
523
524
525
            host,
            port,
            worker_urls,
526
            policy,
527
            worker_startup_timeout_secs,
528
            worker_startup_check_interval,
529
            cache_threshold,
530
531
            balance_abs_threshold,
            balance_rel_threshold,
532
533
            eviction_interval_secs,
            max_tree_size,
534
            max_payload_size,
535
536
            dp_aware,
            api_key,
537
            log_dir,
538
            log_level,
539
540
541
542
            service_discovery,
            selector,
            service_discovery_port,
            service_discovery_namespace,
543
544
545
            prefill_selector,
            decode_selector,
            bootstrap_port_annotation,
546
547
            prometheus_port,
            prometheus_host,
548
            request_timeout_secs,
549
            request_id_headers,
550
            pd_disaggregation,
551
552
            prefill_urls,
            decode_urls,
553
554
            prefill_policy,
            decode_policy,
555
556
            max_concurrent_requests,
            cors_allowed_origins,
557
558
559
560
561
562
563
564
565
566
567
            retry_max_retries,
            retry_initial_backoff_ms,
            retry_max_backoff_ms,
            retry_backoff_multiplier,
            retry_jitter_factor,
            disable_retries,
            cb_failure_threshold,
            cb_success_threshold,
            cb_timeout_duration_secs,
            cb_window_duration_secs,
            disable_circuit_breaker,
568
569
570
571
572
            health_failure_threshold,
            health_success_threshold,
            health_check_timeout_secs,
            health_check_interval_secs,
            health_check_endpoint,
573
            enable_igw,
574
575
576
            queue_size,
            queue_timeout_secs,
            rate_limit_tokens_per_second,
577
578
579
            connection_mode,
            model_path,
            tokenizer_path,
580
            chat_template,
581
582
583
584
            tokenizer_cache_enable_l0,
            tokenizer_cache_l0_max_entries,
            tokenizer_cache_enable_l1,
            tokenizer_cache_l1_max_memory,
585
586
            reasoning_parser,
            tool_call_parser,
587
588
589
            backend,
            history_backend,
            oracle_config,
590
        })
591
592
593
    }

    fn start(&self) -> PyResult<()> {
594
595
596
        let router_config = self.to_router_config().map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("Configuration error: {}", e))
        })?;
597

598
599
600
601
602
603
        router_config.validate().map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!(
                "Configuration validation failed: {}",
                e
            ))
        })?;
604

605
606
607
608
609
610
611
        let service_discovery_config = if self.service_discovery {
            Some(service_discovery::ServiceDiscoveryConfig {
                enabled: true,
                selector: self.selector.clone(),
                check_interval: std::time::Duration::from_secs(60),
                port: self.service_discovery_port,
                namespace: self.service_discovery_namespace.clone(),
612
613
614
615
                pd_mode: self.pd_disaggregation,
                prefill_selector: self.prefill_selector.clone(),
                decode_selector: self.decode_selector.clone(),
                bootstrap_port_annotation: self.bootstrap_port_annotation.clone(),
616
617
618
619
620
            })
        } else {
            None
        };

621
622
623
624
625
626
627
628
        let prometheus_config = Some(PrometheusConfig {
            port: self.prometheus_port.unwrap_or(29000),
            host: self
                .prometheus_host
                .clone()
                .unwrap_or_else(|| "127.0.0.1".to_string()),
        });

629
630
631
632
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;

        runtime.block_on(async move {
633
634
635
            server::startup(server::ServerConfig {
                host: self.host.clone(),
                port: self.port,
636
                router_config,
637
                max_payload_size: self.max_payload_size,
638
                log_dir: self.log_dir.clone(),
639
                log_level: self.log_level.clone(),
640
                service_discovery_config,
641
                prometheus_config,
642
                request_timeout_secs: self.request_timeout_secs,
643
                request_id_headers: self.request_id_headers.clone(),
644
645
            })
            .await
646
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
647
        })
648
649
650
651
    }
}

#[pymodule]
652
fn sglang_router_rs(m: &Bound<'_, PyModule>) -> PyResult<()> {
653
    m.add_class::<PolicyType>()?;
654
655
656
    m.add_class::<BackendType>()?;
    m.add_class::<HistoryBackendType>()?;
    m.add_class::<PyOracleConfig>()?;
657
658
    m.add_class::<Router>()?;
    Ok(())
659
}