Skip to content

Library Usage

Continuum Router can be used as an embeddable Rust library crate in addition to the standalone CLI binary. This enables you to integrate LLM routing directly into your Rust application without spawning a separate process.

Use Cases

  • Embedding in existing applications: Integrate LLM routing into your Axum/Tokio application
  • Programmatic configuration: Configure the router entirely in Rust code without YAML files
  • Custom extensions: Add custom middleware, routes, or modify behavior around the router
  • Integration testing: Create router instances programmatically for test suites

Adding the Dependency

Add continuum-router as a library dependency in your Cargo.toml:

[dependencies]
continuum-router = { git = "https://github.com/lablup/continuum-router.git" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
axum = "0.7"

To reduce compile times and binary size, you can disable default features and select only what you need:

[dependencies]
continuum-router = { git = "https://github.com/lablup/continuum-router.git", default-features = false, features = ["metrics", "hot-reload"] }

See Cargo Feature Flags for the full list of available features.

Quick Start

Standalone Server from Config File

The simplest way to use the library is to load a YAML config file and run the server:

use continuum_router::ContinuumRouter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize tracing (optional - the library does not initialize tracing)
    tracing_subscriber::fmt::init();

    let router = ContinuumRouter::from_config_file("config.yaml")
        .await?
        .enable_hot_reload(true)
        .build()
        .await?;

    router.serve("0.0.0.0:8080").await?;
    Ok(())
}

Standalone Server from Programmatic Config

You can also configure the router entirely in code:

use continuum_router::{ContinuumRouter, Config};
use continuum_router::config::{BackendConfig, BackendTypeConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let mut config = Config::default();
    config.backends = vec![
        BackendConfig {
            name: "openai".to_string(),
            url: "https://api.openai.com/v1".to_string(),
            backend_type: BackendTypeConfig::Openai,
            api_key: std::env::var("OPENAI_API_KEY").ok(),
            models: vec!["gpt-4o".to_string()],
            ..BackendConfig::default()
        },
        BackendConfig {
            name: "ollama".to_string(),
            url: "http://localhost:11434".to_string(),
            backend_type: BackendTypeConfig::Ollama,
            models: vec![],  // Auto-discovered
            ..BackendConfig::default()
        },
    ];

    let router = ContinuumRouter::from_config(config)
        .enable_health_checks(true)
        .enable_circuit_breaker(true)
        .build()
        .await?;

    router.serve("0.0.0.0:8080").await?;
    Ok(())
}

Type-Safe Config Builders

For a cleaner alternative to constructing BackendConfig structs manually, use the type-safe builder API. Builders validate all settings at build time and provide provider-specific constructors:

use continuum_router::config::builder::{BackendConfigBuilder, ConfigBuilder};
use continuum_router::ContinuumRouter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    let openai = BackendConfigBuilder::openai("https://api.openai.com/v1", "sk-...")
        .name("primary-openai")
        .models(vec!["gpt-4o", "gpt-4o-mini"])
        .build()?;

    let ollama = BackendConfigBuilder::ollama("http://localhost:11434")
        .build()?;

    let config = ConfigBuilder::new()
        .add_backend(openai)
        .add_backend(ollama)
        .bind_address("0.0.0.0:8080")
        .enable_health_checks(true)
        .logging_level("info")
        .build()?;

    let router = ContinuumRouter::from_config(config)
        .build()
        .await?;

    router.serve("0.0.0.0:8080").await?;
    Ok(())
}

See the full Rust Builder API reference for all available builder methods, provider constructors, and error types.

Embed in an Existing Axum Application

Use into_router() to get an Axum Router that you can nest into your own application:

use axum::{routing::get, Router};
use continuum_router::ContinuumRouter;

async fn custom_handler() -> &'static str {
    "Custom endpoint"
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt::init();

    // Build the Continuum Router
    let llm_router = ContinuumRouter::from_config_file("config.yaml")
        .await?
        .build()
        .await?;

    // Nest it under /llm alongside your own routes
    let app = Router::new()
        .route("/custom", get(custom_handler))
        .nest("/llm", llm_router.into_router());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
    axum::serve(listener, app).await?;
    Ok(())
}

When embedded, the router's endpoints become available under the nest prefix. For example, /llm/v1/chat/completions, /llm/v1/models, /llm/admin/backends, etc.

Builder API Reference

The builder is created via ContinuumRouter::from_config() or ContinuumRouter::from_config_file(), and configured with a fluent method chain before calling .build().await.

Entry Points

ContinuumRouter::from_config(config: Config) -> ContinuumRouterBuilder

Create a builder from a programmatic Config struct. No file watching or hot-reload is available unless a ConfigManager is attached manually via with_config_manager().

ContinuumRouter::from_config_file(path: &str) -> Result<ContinuumRouterBuilder, RouterError>

Create a builder by loading configuration from a YAML/TOML file. This internally creates a ConfigManager that supports hot-reload when enabled. The config directory is automatically derived from the file path for prompt file resolution.

Builder Methods

All builder methods return Self for chaining.

Method Description Default
enable_hot_reload(bool) Enable/disable config file watching and automatic reload Respects config
enable_health_checks(bool) Enable/disable background health monitoring for backends Enabled
enable_circuit_breaker(bool) Enable/disable the circuit breaker state machine Respects config
enable_metrics(bool) Enable/disable Prometheus metrics collection Respects config
enable_files_api(bool) Enable/disable the /v1/files upload API Respects config
with_http_client(client) Inject a pre-configured reqwest::Client Auto-created
with_config_dir(path) Set the base directory for prompt file resolution Derived from config path
with_config_manager(manager) Attach an externally created ConfigManager None

Override Semantics

When a builder method is not called, the builder respects whatever the config file or Config::default() specifies. When a method is called, the explicit value wins regardless of the config source.

ContinuumRouter Methods

After building, you get a ContinuumRouter instance with these methods:

Method Description
serve(addr: &str) Run as a standalone server; blocks until shutdown signal (SIGINT/SIGTERM)
into_router() -> Router Return the Axum Router for embedding into another application
state() -> Arc<AppState> Access the shared application state
config_handle() -> Option<&ConfigManager> Access the ConfigManager (only when loaded from a file)
shutdown() Consume and drop the router, releasing all resources

Hot-Reload in Library Mode

Continuum Router's hot-reload infrastructure works in library mode just as it does for the CLI binary.

File-Based Hot-Reload

When you use from_config_file() with hot-reload enabled, the router watches the config file for changes and automatically applies updates:

let router = ContinuumRouter::from_config_file("config.yaml")
    .await?
    .enable_hot_reload(true)
    .build()
    .await?;

Any changes to config.yaml are detected via filesystem notifications and propagated to all components (backends, health checker, circuit breaker, rate limiter, model service).

Programmatic Runtime Updates

You can also update configuration at runtime via the ConfigManager:

let router = ContinuumRouter::from_config_file("config.yaml")
    .await?
    .enable_hot_reload(true)
    .build()
    .await?;

if let Some(config_handle) = router.config_handle() {
    // Get current config
    let mut new_config = config_handle.get_config().await;

    // Modify it
    new_config.backends.push(BackendConfig {
        name: "new-backend".to_string(),
        url: "http://localhost:8000".to_string(),
        ..BackendConfig::default()
    });

    // Apply the update (triggers HotReloadService)
    config_handle.set_config(new_config).await?;
}

Subscribing to Config Changes

You can subscribe to configuration change notifications using the watch channel pattern:

if let Some(config_handle) = router.config_handle() {
    let mut rx = config_handle.subscribe();

    tokio::spawn(async move {
        while rx.changed().await.is_ok() {
            let config = rx.borrow().clone();
            println!("Config updated: {} backends", config.backends.len());
        }
    });
}

Hot-Reloadable Settings

Not all settings can be changed at runtime. Here is what supports hot-reload:

Immediate effect:

  • Logging level
  • Rate limiting settings
  • Circuit breaker settings
  • Retry settings
  • Global prompts

Gradual effect (new connections use new config):

  • Backend add/remove/modify (with graceful draining)
  • Health check settings
  • Timeout settings
  • Selection strategy

Requires restart:

  • Server bind address
  • Worker count
  • Connection pool size

Accessing Internal State

The state() method provides access to the shared AppState, which holds references to all internal components:

let router = ContinuumRouter::from_config_file("config.yaml")
    .await?
    .build()
    .await?;

let state = router.state();

// Check health checker status
if let Some(ref checker) = state.health_checker {
    let status = checker.get_backend_health_status().await;
    for backend in &status {
        println!("{}: {:?}", backend.name, backend.status);
    }
}

// Access the current config
let config = state.current_config();
println!("Selection strategy: {:?}", config.selection_strategy);

Custom HTTP Client

You can provide a pre-configured reqwest::Client for backend communication. This is useful when you need specific TLS settings, proxy configuration, or connection pool tuning:

let client = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(60))
    .connect_timeout(std::time::Duration::from_secs(5))
    .pool_max_idle_per_host(32)
    .build()?;

let router = ContinuumRouter::from_config(config)
    .with_http_client(client)
    .build()
    .await?;

Important Notes

Tracing Initialization

The library does not initialize tracing. You must call tracing_subscriber::fmt::init() (or your preferred tracing setup) before building the router if you want log output.

Metrics Singleton

The Prometheus metrics registry (RouterMetrics) is a process-global singleton. If you embed multiple ContinuumRouter instances in the same process, they will share the same metrics registry. This is a known limitation that may be addressed in a future release.

Graceful Shutdown

When using serve(), the router handles SIGINT and SIGTERM signals automatically and performs graceful shutdown (draining connections, cleaning up Unix sockets, stopping background tasks).

When using into_router() for embedding, you manage the server lifecycle yourself. Call shutdown() on the ContinuumRouter instance when you're done, or simply drop it to release resources.

Runnable Examples

The repository includes four ready-to-run examples in the examples/ directory. Each example can be executed with cargo run --example <name>.

standalone_server

Minimal standalone server that loads a config file and starts serving:

cargo run --example standalone_server -- --config config.yaml

See examples/standalone_server.rs for the full source.

axum_integration

Embeds the router under /llm in a larger Axum application:

cargo run --example axum_integration -- --config config.yaml

After starting, custom endpoints are available at /health and /info, while LLM endpoints are under /llm/v1/....

See examples/axum_integration.rs for the full source.

programmatic_config

Configures the router entirely in Rust code without a YAML file:

OPENAI_API_KEY=sk-... cargo run --example programmatic_config
# or with a local Ollama instance:
OLLAMA_URL=http://localhost:11434 cargo run --example programmatic_config

See examples/programmatic_config.rs for the full source.

hot_reload

Demonstrates file-based hot-reload and subscribing to config-change events:

cargo run --example hot_reload -- --config config.yaml

While the server is running, edit config.yaml to see changes applied automatically.

See examples/hot_reload.rs for the full source.

ACP Support

The ACP (Agent Communication Protocol) module is always compiled into the library crate (pub mod acp). When building custom applications, you can access ACP types, handlers, and transport implementations directly:

use continuum_router::acp::config::AcpConfig;
use continuum_router::acp::router::AcpMethodRouter;
use continuum_router::acp::session::SessionStore;
use continuum_router::acp::transport::stdio::StdioTransport;

The CLI binary's --mode stdio implementation in src/main.rs serves as a reference for how to wire ACP handlers into a custom application. See the ACP Architecture documentation for protocol details and module structure.

Further Reading