콘텐츠로 이동

라이브러리 사용법

Continuum Router는 독립 실행형 CLI 바이너리 외에도 임베딩 가능한 Rust 라이브러리 크레이트로 쓸 수 있습니다. 별도 프로세스를 띄우지 않고 LLM 라우팅을 Rust 애플리케이션 안에 직접 넣을 수 있습니다.

사용 사례

  • 기존 애플리케이션에 임베딩: Axum/Tokio 애플리케이션에 LLM 라우팅 통합
  • 프로그래밍 방식 설정: YAML 파일 없이 Rust 코드로 라우터 설정
  • 커스텀 확장: 라우터 주변에 커스텀 미들웨어, 라우트 추가 또는 동작 수정
  • 통합 테스트: 테스트 스위트를 위한 라우터 인스턴스 프로그래밍 방식 생성

의존성 추가

Cargo.toml에 continuum-router를 라이브러리 의존성으로 추가합니다:

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

컴파일 시간과 바이너리 크기를 줄이려면, 기본 기능을 비활성화하고 필요한 것만 선택할 수 있습니다:

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

사용 가능한 전체 기능 목록은 Cargo 기능 플래그를 참조하세요.

빠른 시작

설정 파일로 독립 실행 서버

라이브러리를 사용하는 가장 간단한 방법은 YAML 설정 파일을 로드하고 서버를 실행하는 것입니다:

use continuum_router::ContinuumRouter;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 트레이싱 초기화 (선택사항 - 라이브러리는 트레이싱을 초기화하지 않음)
    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(())
}

프로그래밍 방식 설정으로 독립 실행 서버

코드에서 라우터를 완전히 설정할 수도 있습니다. BackendConfig에는 Default 구현이 없으므로 구조체 리터럴에 모든 필드를 적어야 하며 ..Default::default() 축약형은 쓸 수 없습니다. 너무 장황하다면 아래의 타입 안전 빌더를 쓰는 편이 낫습니다. 백엔드를 직접 만들 때 권장하는 방식입니다. 이 예제를 실제로 컴파일한 코드는 examples/programmatic_config.rs에 있습니다.

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(),
            backend_id: None,
            backend_type: BackendTypeConfig::Openai,
            url: "https://api.openai.com/v1".to_string(),
            weight: 1,
            api_key: std::env::var("OPENAI_API_KEY").ok(),
            org_id: None,
            auth: None,
            models: vec!["gpt-5.6-sol".to_string()],
            model_configs: vec![],
            retry_override: None,
            health_check: None,
            anthropic_auto_cache_control: None,
            forward_cache_control: None,
            anthropic_fast_mode: None,
            endpoint_type: None,
            region: None,
            role: Default::default(),
            external_storage: None,
            internal: false,
        },
        BackendConfig {
            name: "ollama".to_string(),
            backend_id: None,
            backend_type: BackendTypeConfig::Ollama,
            url: "http://localhost:11434".to_string(),
            weight: 1,
            api_key: None,
            org_id: None,
            auth: None,
            models: vec![],  // 자동 검색
            model_configs: vec![],
            retry_override: None,
            health_check: None,
            anthropic_auto_cache_control: None,
            forward_cache_control: None,
            anthropic_fast_mode: None,
            endpoint_type: None,
            region: None,
            role: Default::default(),
            external_storage: None,
            internal: false,
        },
    ];

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

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

타입 안전 설정 빌더

BackendConfig 구조체를 수동으로 구성하는 대신, 타입 안전 빌더 API를 사용하면 더 깔끔합니다. 빌더는 빌드 시점에 모든 설정을 검증하고 프로바이더별 생성자를 제공합니다:

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-5.6-sol", "gpt-5.6-terra"])
        .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(())
}

모든 빌더 메서드, 프로바이더 생성자, 오류 타입에 대한 전체 레퍼런스는 Rust Builder API를 참조하세요.

기존 Axum 애플리케이션에 임베딩

into_router()를 사용하여 기존 애플리케이션에 중첩할 수 있는 Axum Router를 가져옵니다:

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();

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

    // /llm 하위에 중첩하고 커스텀 라우트와 함께 사용
    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(())
}

임베딩 시 라우터의 엔드포인트는 중첩 프리픽스 하위에서 사용할 수 있습니다. 예: /llm/v1/chat/completions, /llm/v1/models, /llm/admin/backends 등.

iOS 및 모바일용 인프로세스 임베딩

호스트 프로세스가 라우터를 사이드카 서브프로세스로 띄울 수 없는 환경(예: 앱 샌드박스가 자식 프로세스 생성을 금지하는 iOS)에서는 continuum-router를 Rust 라이브러리로 링크하고, 앱 내부 WebView가 바라보는 루프백 주소에서 HTTP 서버를 인프로세스로 실행할 수 있습니다.

모바일 타깃으로 크로스 컴파일하기 까다롭거나 불가능한 크레이트가 의존성 그래프에 포함되지 않도록 최소 기능인 embed 피처를 사용하세요:

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

embed 피처는 추가 컴포넌트를 켜지 않으며 iOS 환경에 부적합한 선택적 의존성(rusqlite, redis, prometheus, rust-embed, aws-*, clap, notify, rmcp, object_store, gcp_auth)을 모두 배제합니다. gcp_auth는 Vertex 경유 Gemini 연동에만 필요하며, 공개 Gemini API(generativelanguage.googleapis.com, API 키 방식) 경로에는 필요하지 않으므로 제외됩니다.

진입점은 cli 피처 없이도 호출할 수 있도록 기본 빌더 경로를 얇게 감싼 비동기 래퍼 함수 serve_embedded입니다:

use continuum_router::{serve_embedded, Config};

async fn run(config: Config) -> Result<(), Box<dyn std::error::Error>> {
    // SIGINT/SIGTERM까지 대기하며, 모바일 환경에서는 호스트가 런타임 수명주기를 제어합니다.
    serve_embedded(config, "127.0.0.1:8080").await?;
    Ok(())
}

serve_embedded(config, addr)는 다음 코드와 완전히 동일합니다:

use continuum_router::{Config, ContinuumRouter};

async fn run(config: Config, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
    ContinuumRouter::from_config(config)
        .build()
        .await?
        .serve(addr)
        .await?;
    Ok(())
}

자체 리스너 바인딩, 그레이스풀 셧다운 제어, 기존 Axum 앱 하위 중첩 등 더 세밀한 수명주기 제어가 필요하다면 기본 from_config(...).build() 경로와 into_router()를 함께 사용하세요. 경량화된 의존성 그래프는 다음 명령으로 검증할 수 있습니다:

cargo tree --no-default-features --features embed --edges normal --target aarch64-apple-ios

빌더 API 레퍼런스

빌더는 ContinuumRouter::from_config() 또는 ContinuumRouter::from_config_file()로 생성하고 .build().await 호출 전에 메서드 체인으로 설정합니다.

진입점

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

프로그래밍 방식의 Config 구조체에서 빌더를 생성합니다. with_config_manager()로 ConfigManager를 수동으로 연결하지 않는 한 파일 감시 및 핫 리로드를 사용할 수 없습니다.

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

YAML/TOML 파일에서 설정을 로드하여 빌더를 생성합니다. 내부적으로 ConfigManager를 생성하며 활성화 시 핫 리로드를 지원합니다. 프롬프트 파일 해석을 위한 설정 디렉터리는 파일 경로에서 자동으로 유도됩니다.

빌더 메서드

모든 빌더 메서드는 체이닝을 위해 Self를 반환합니다.

메서드 설명 기본값
enable_hot_reload(bool) 설정 파일 감시 및 자동 리로드 활성화/비활성화 설정에 따름
enable_health_checks(bool) 백엔드 백그라운드 헬스 모니터링 활성화/비활성화 활성화
enable_circuit_breaker(bool) 프록시 선택/결과 기록과 Admin 상태 제어가 사용하는 서킷 브레이커 활성화/비활성화 설정에 따름
enable_metrics(bool) Prometheus 메트릭 수집 활성화/비활성화 설정에 따름
enable_files_api(bool) /v1/files 업로드 API 활성화/비활성화 설정에 따름
without_control_plane_agent() serve()를 호출하지 않는다고 선언하므로 control-plane 사용량 채널과 사용량 write-ahead 로그를 만들지 않습니다. 에이전트를 직접 실행하지 않는 임베더가 into_router()와 함께 사용합니다 꺼짐
with_http_client(client) 사전 설정된 reqwest::Client 주입 자동 생성
with_probe_http_client(client) 헬스 체크와 모델 카탈로그 조회에 쓰는 reqwest::Client 주입 별도 풀로 자동 생성, with_http_client만 설정하면 그 클라이언트를 공유
with_config_dir(path) 프롬프트 파일 해석을 위한 기본 디렉터리 설정 설정 파일 경로에서 유도
with_config_manager(manager) 외부에서 생성한 ConfigManager 연결 None

오버라이드 규칙

빌더 메서드를 호출하지 않으면, 설정 파일 또는 Config::default()에 지정된 값을 따릅니다. 메서드를 호출하면, 설정 소스에 관계없이 명시적 값이 우선합니다.

ContinuumRouter 메서드

빌드 후 다음 메서드를 가진 ContinuumRouter 인스턴스를 얻습니다:

메서드 설명
serve(addr: &str) 독립 실행 서버로 실행; 종료 신호(SIGINT/SIGTERM)까지 블로킹
into_router() -> Router 다른 애플리케이션에 임베딩하기 위한 Axum Router 반환
state() -> Arc<AppState> 공유 애플리케이션 상태 접근
config_handle() -> Option<&ConfigManager> ConfigManager 접근 (파일에서 로드한 경우에만)
shutdown() 라우터를 소비하고 드롭하여 모든 리소스 해제

라이브러리 모드에서의 핫 리로드

Continuum Router의 핫 리로드 인프라는 CLI 바이너리와 동일하게 라이브러리 모드에서도 작동합니다.

파일 기반 핫 리로드

from_config_file()에 핫 리로드를 활성화하면, 라우터가 설정 파일의 변경을 감시하고 자동으로 업데이트를 적용합니다:

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

config.yaml의 유효한 변경은 파일시스템 알림으로 감지되어 새 설정 스냅샷으로 발행됩니다. 백엔드와 상태 확인은 점진적으로 갱신되고 활성 속도 제한·프롬프트·요청 매개변수·스트리밍·스마트 라우팅 및 일부 정책 서비스는 지원되는 동적 필드를 사용합니다. 시작 시 생성된 서비스는 재시작할 때까지 시작 값을 유지합니다.

프로그래밍 방식 런타임 발행

config_handle()은 공유 조회/구독 핸들을 반환합니다. 변경 가능한 ConfigManager::set_config 작업은 노출하지 않습니다. 런타임 개정을 발행하려면 Admin 섹션 API, apply: true인 가져오기, 또는 감시 중인 파일 갱신을 사용하세요.

설정 변경 구독

watch 채널 패턴을 사용하여 설정 변경 알림을 구독할 수 있습니다:

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.backends.len());
        }
    });
}

핫 리로드 가능 설정

보수적인 런타임 분류는 다음과 같습니다.

해당 서비스가 존재할 때 즉시 적용:

  • 속도 제한과 프록시 데이터 경로의 서킷 정책
  • 전역 프롬프트와 요청 매개변수 정책
  • 스트리밍과 스마트 라우팅 정책 스냅샷
  • 선택 전략, 재시도 정책, 요청별 타임아웃 예산
  • api_keys.mode, 모든 프리픽스 라우팅 필드, 폴백 체인과 폴백 정책

점진적 적용:

  • 그레이스풀 드레이닝을 사용하는 백엔드 추가/제거/수정
  • 상태 확인 설정

재시작 필요 또는 실시간 소비자 없음:

  • 서버 리스너, 워커, 연결 풀 구성, CORS
  • 로깅과 추적 구성
  • timeouts.connection과 timeouts.request.streaming.total(공유 HTTP 클라이언트 상한이며 요청별 예산은 실시간 반영)
  • 파일 감시로 바뀐 API 키 값과 시작 시 생성된 캐시/서비스 객체

실행 중인 빌드의 분류는 /admin/config/hot-reload-status에서 확인하세요.

라이브러리 모드의 허브 거버넌스

Continuum Hub의 가드레일 거버넌스(guardrail_policy_v1, guardrail_provider_content_v1의 허브 작성 프로바이더 콘텐츠 포함)와 스마트 라우팅 거버넌스(smart_routing_policy_v1)를 사용하려면 다음 세 가지 요건이 충족되어야 합니다:

  • 컴파일 시점에 활성화된 control-plane Cargo 기능 플래그
  • 설정 파일에서 control_plane.enabled 및 control_plane.policy.enabled를 모두 true로 설정
  • 라우터에 연결된 활성 설정 관리자(ConfigManager). 허브 정책은 최신 로컬 설정을 덮어쓰지 않고 로컬 설정과 병합되므로, 두 거버넌스 동기화 모듈(reconciler) 모두 설정 관리자의 변경 스트림을 구독합니다.

라우터를 생성하는 방식에 따라 동기화 모듈 연결 여부가 결정됩니다:

  • ConfigManager 및 핫 리로드 활성화: 핫 리로드를 켠 상태로 from_config_file()을 통해 생성한 라우터는 기본 CLI 바이너리와 동일하게 두 동기화 모듈을 모두 연결합니다.
  • ConfigManager 미사용 또는 핫 리로드 비활성화: with_config_manager() 없이 호출한 ContinuumRouter::from_config(config), serve_embedded, 또는 enable_hot_reload(false)를 지정한 빌더는 두 동기화 모듈을 연결하지 않으며 해당 기능도 허브에 노출하지 않습니다. 이 경우 허브는 응답 없는 확인을 무한정 기다리는 대신 두 정책 모두를 unsupported 상태로 표시하며, 로컬 guardrails:와 smart_routing: 섹션이 전체 정책으로 동작합니다.
  • 모바일 embed 빌드: embed 기능 플래그에는 control-plane이 포함되지 않으므로, 임베디드 모바일 빌드는 허브 에이전트를 실행하지 않습니다.

자세한 내용은 스마트 라우팅 거버넌스와 가드레일: 허브 거버넌스 정책을 참고하세요.

내부 상태 접근

state() 메서드는 모든 내부 컴포넌트에 대한 참조를 가진 공유 AppState에 대한 접근을 제공합니다:

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

let state = router.state();

// 헬스 체커 상태 확인
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);
    }
}

// 현재 설정 접근
let config = state.current_config();
println!("선택 전략: {:?}", config.selection_strategy);

커스텀 HTTP 클라이언트

백엔드 통신을 위해 사전 설정된 reqwest::Client를 제공할 수 있습니다. 특정 TLS 설정, 프록시 구성, 또는 연결 풀 튜닝이 필요할 때 유용합니다:

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?;

헬스 체크와 모델 카탈로그 조회는 프록시 스트림이 백엔드 연결의 동시 스트림 한도를 모두 채우더라도 프로브 요청이 대기하다 타임아웃되지 않도록 별도의 연결 풀을 가진 전용 HTTP 클라이언트에서 실행됩니다:

  • 기본 클라이언트 구성: 커스텀 클라이언트를 주입하지 않으면 라우터는 프록시 클라이언트와 동일한 설정으로 프로브 전용 클라이언트를 자동 생성합니다.
  • 커스텀 클라이언트 주입: with_http_client로 클라이언트를 주입하면 커스텀 TLS나 프록시 설정을 설정 객체에서 다시 복원할 수 없으므로 프로브도 기본적으로 해당 클라이언트를 공유합니다. 프로브를 독립된 연결 풀로 분리하려면 동일한 방식으로 두 번째 클라이언트를 생성해 with_probe_http_client에 전달하세요:
let build = || {
    reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(60))
        .connect_timeout(std::time::Duration::from_secs(5))
        .build()
};

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

주의 사항

트레이싱 초기화

라이브러리는 트레이싱을 초기화하지 않습니다. 로그 출력을 원하면 라우터를 빌드하기 전에 tracing_subscriber::fmt::init() (또는 선호하는 트레이싱 설정)을 호출해야 합니다.

메트릭 싱글턴

Prometheus 메트릭 레지스트리(RouterMetrics)는 프로세스 전역 싱글턴입니다. 동일 프로세스에 여러 ContinuumRouter 인스턴스를 임베딩하면 동일한 메트릭 레지스트리를 공유합니다.

그레이스풀 셧다운

serve() 또는 serve_embedded() 사용 시, 라우터는 SIGINT 및 SIGTERM 신호를 자동으로 처리하고 그레이스풀 셧다운을 수행합니다. 모든 리스너가 새 연결 수락을 즉시 멈추고, 유휴 keep-alive 연결은 닫히며, 이미 처리 중인 요청(스트림 포함)은 끝나거나 server.shutdown_drain_timeout(기본값 20s)이 지날 때까지 계속 실행되고, 그 이후 남은 연결은 중단됩니다. 드레인 중 두 번째 신호를 받으면 즉시 중단합니다. 백그라운드 작업은 드레인이 끝난 뒤 멈추고, Unix 소켓 파일은 삭제됩니다.

into_router()로 임베딩할 때는 서버 라이프사이클을 직접 관리합니다. 완료 시 ContinuumRouter 인스턴스에서 shutdown()을 호출하거나, 단순히 드롭하여 리소스를 해제하세요.

실행 가능한 예제

저장소의 examples/ 디렉토리에는 바로 실행할 수 있는 네 가지 예제가 포함되어 있습니다. 각 예제는 cargo run --example <이름>으로 실행할 수 있습니다.

standalone_server

설정 파일을 로드하여 서버를 시작하는 최소한의 예제입니다:

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

전체 소스는 examples/standalone_server.rs를 참조하세요.

axum_integration

더 큰 Axum 애플리케이션에 라우터를 /llm 경로 아래에 임베딩합니다:

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

시작 후, 커스텀 엔드포인트는 /health 및 /info에서, LLM 엔드포인트는 /llm/v1/...에서 사용할 수 있습니다.

전체 소스는 examples/axum_integration.rs를 참조하세요.

programmatic_config

YAML 파일 없이 Rust 코드만으로 라우터를 설정합니다:

OPENAI_API_KEY=sk-... cargo run --example programmatic_config
# 또는 로컬 Ollama 인스턴스 사용 시:
OLLAMA_URL=http://localhost:11434 cargo run --example programmatic_config

전체 소스는 examples/programmatic_config.rs를 참조하세요.

hot_reload

파일 기반 핫 리로드 및 설정 변경 이벤트 구독을 시연합니다:

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

서버 실행 중에 config.yaml을 편집하면 변경 사항이 자동으로 적용됩니다.

전체 소스는 examples/hot_reload.rs를 참조하세요.

ACP 지원

ACP (Agent Communication Protocol) 모듈은 항상 라이브러리 크레이트에 컴파일됩니다 (pub mod acp). 커스텀 애플리케이션을 빌드할 때 ACP 타입, 핸들러, 전송 구현에 직접 접근할 수 있습니다:

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;

CLI 바이너리의 --mode stdio 구현(src/main.rs)은 ACP 핸들러를 커스텀 애플리케이션에 연결하는 방법에 대한 참조입니다. 프로토콜 세부 사항 및 모듈 구조에 대해서는 ACP 아키텍처 문서를 참조하세요.

추가 참고