┌─────────────────────────────────────────────────────────────┐
│ apps/rook │ ← main.rs, config.rs, di.rs
│ (binary, DI bootstrap) │
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ transport-axum (infrastructure) │ ← HTTP server, route handlers
│ openai_adapter, anthropic_adapter │ ← wire format ↔ domain model
└────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────┐
│ rook-usecases (application) │ ← RouteRequest, FallbackRouter
│ ManageProviders, HealthCheck, ManageConnections │
└────────────────────────┬────────────────────────────────────┘
│
┌───────────────┼───────────────┬───────────────┐
│ │ │ │
┌────────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼───────┐
│providers-openai││providers- │ │providers- │ │providers- │
│providers- │ │anthropic │ │ollama │ │gemini, groq │
│anthropic │ │ │ │ │ │ │
└────────────────┘ └────────────┘ └─────────────┘ └──────────────┘
↓ ↓ ↓ ↓
┌───────────────────────────────────────────────────────────────┐
│ rook-core (domain) │ ← CompletionRequest/Response,
│ ports.rs, model.rs │ ProviderPort, RouterPort,
└────────────────────────┬────────────────────────────────────┘ CachePort, AuditPort
│
┌────────────────────────▼────────────────────────────────────┐
│ shared-kernel │ ← no external deps
│ id.rs, error.rs, time_.rs │
└─────────────────────────────────────────────────────────────┘
Common types with zero external dependencies.
ProviderId,ModelId,RequestId— newtype wrappers (prevents mixing at type level)CortexError— error types: ProviderError, NotFoundError, RateLimitedError, AllProvidersExhaustedErrorCacheKey— derived from request ID for caching
Domain model and port traits. Completely provider-agnostic.
- Model —
CompletionRequest,CompletionResponse,Message,Role,TokenUsage,StreamChunk,HealthStatus,AuditEntry - Ports — capability traits the domain requires but cannot implement:
ProviderPort— LLM provider capability (complete, stream, health_check)RouterPort— provider selection with failure notificationCachePort— get/set/delete/clear with TTLAuditPort— record audit entriesProviderRepositoryPort— persisted provider connection storageProviderRegistryPort— runtime provider lookup for health probesKeyManager— credential encryption boundary
Application orchestration.
RouteRequest— the main orchestrator: cache → select provider → execute → cache response → audit → handle failure. Supports combo execution for multi-step fallback chains.FallbackRouter— implements RouterPort with three strategies: Priority, RoundRobin, ModelBased. Includes circuit breaker (3 failures → 30s cooldown).ManageProviders— enable/disable providers (interface only for now)HealthCheck— aggregated health status across all providersManageConnections— runtime-managed provider connection CRUD/test workflow
Combos provide automatic failover across multiple provider/model pairs. They are managed as first-class domain entities with:
- Domain model (
rook-core) —Combo,ComboStep,ComboStrategy, validation rules - Repository (
combo-sqlite) — SQLite-backed persistence with name/ID uniqueness - API (
transport-axum/combo_routes) — CRUD endpoints at/api/combos - Execution (
RouteRequest::execute_combo) — priority-ordered step execution with timeouts, circuit breaker integration, and comprehensive audit/usage tracking - Configuration (
apps/rook/config) — TOML-based combo definitions seeded at startup
Execution Flow:
Request with X-Rook-Combo header or default_combo
→ Load combo from repository
→ Sort steps by priority (ascending)
→ For each step:
→ Check circuit breaker (skip if open)
→ Check provider availability (skip if not registered)
→ Execute with 10s timeout
→ On success: return immediately
→ On 4xx (except 429): stop, return error
→ On 429/5xx/network: continue to next step
→ If all steps fail: AllProvidersExhausted error
Timeouts:
- Per-step timeout: 10 seconds
- Overall combo timeout: 60 seconds
Streaming Limitation: Combos only apply before streaming starts. Once the first chunk is sent, no fallback occurs.
HTTP transport layer. All wire-format logic lives here.
routes.rs— axum router with four endpointsopenai_adapter.rs— OpenAI wire format ↔ domain model translationanthropic_adapter.rs— Anthropic/v1/messageswire format ↔ domain modelprovider_routes.rs—/api/providersCRUD endpoints, mounted only when enabledprovider_dto.rs— provider connection JSON DTOs; responses always includecredentials: {}
Provider crates (providers-openai, providers-anthropic, providers-ollama, providers-gemini, providers-groq)
Each implements ProviderPort for a specific API. All share the same structure:
- Config struct (id, api_key, base_url, models list, timeout_secs)
new()→Arc<Self>is_available()— synchronous check (e.g., non-empty API key)health_check()— async, returnsHealthStatuswith latencycomplete()— makes the actual API call viareqwest::Clientstream()— stub in all providers except OpenAI (not yet implemented)
DashMap-based in-memory cache with TTL support. Implements CachePort.
SQLite-backed audit log. Implements AuditPort. Auto-creates schema on init with indexes on request_id, provider, timestamp.
AES-256-GCM credential encryption with Argon2id key derivation. Implements KeyManager.
SQLite-backed provider connection repository. Implements ProviderRepositoryPort.
Binary crate. Assembles all infrastructure.
config.rs— loadsRookConfigfrom TOML, expands~in paths, expands${ENV_VAR}in api_keydi.rs—RookContainer::build()— builds all providers, cache, audit, router, usecases. Single place where all crates are assembled.server.rs— axum server bootstrap with graceful shutdownmain.rs— init tracing → load config → build container → start server
#[async_trait]
pub trait ProviderPort: Send + Sync + 'static {
fn id(&self) -> &ProviderId;
fn supported_models(&self) -> &[ModelId];
fn is_available(&self) -> bool;
async fn health_check(&self) -> HealthStatus;
async fn complete(&self, req: &CompletionRequest) -> CortexResult<CompletionResponse>;
async fn stream(&self, req: &CompletionRequest) -> CortexResult<BoxStream<'_, CortexResult<StreamChunk>>>;
}#[async_trait]
pub trait RouterPort: Send + Sync {
async fn select(&self, req: &CompletionRequest) -> CortexResult<Arc<dyn ProviderPort>>;
async fn on_failure(&self, provider: &ProviderId, error: &CortexError);
fn providers(&self) -> Vec<ProviderId>;
}#[async_trait]
pub trait CachePort: Send + Sync {
async fn get(&self, key: &CacheKey) -> CortexResult<Option<CompletionResponse>>;
async fn set(&self, key: &CacheKey, value: &CompletionResponse, ttl: Duration) -> CortexResult<()>;
async fn delete(&self, key: &CacheKey) -> CortexResult<()>;
async fn clear(&self) -> CortexResult<()>;
}#[async_trait]
pub trait AuditPort: Send + Sync {
async fn record(&self, entry: AuditEntry) -> CortexResult<()>;
}Client HTTP Request
│
▼
transport-axum/routes.rs ─── OpenAI/Anthropic adapter (wire format → domain)
│
▼
rook-usecases/RouteRequest::execute(req)
│
├─ CachePort::get(cache_key) ← TTL cache (DashMap)
│
▼
FallbackRouter::select(req) ← circuit breaker + strategy
│
▼
ProviderPort::complete(req) ← actual API call (reqwest)
│
├─ on success:
│ ├─ CachePort::set(cache_key, resp, ttl)
│ └─ AuditPort::record(success entry)
│
└─ on failure:
├─ RouterPort::on_failure(provider_id, error) ← circuit breaker
└─ AuditPort::record(failure entry)
│
▼
transport-axum ─── domain response → wire format
│
▼
Client HTTP Response
rook.toml file
│
▼
config::RookConfig::load() ← toml::from_str + path expansion
│
├─ Expands ~ in database.db_path to $HOME
├─ Expands ${ENV_VAR} in provider.api_key
│
▼
di::RookContainer::build(&config) ← assembles all infrastructure
│
├─ build_provider(pc) per provider ← maps config.kind → provider impl
├─ InMemoryCache or NoOpCache
├─ SqliteAudit::new(database.db_path)
├─ FallbackRouter::new(providers, strategy)
├─ If auth.api_keys.enabled:
│ ├─ require API_KEY_HASH_SECRET
│ ├─ SqliteApiKeyRepository(database.db_path)
│ └─ AuthenticateClientApi
├─ If provider_crud.enabled:
│ ├─ require ENCRYPTION_PASSPHRASE and ENCRYPTION_SALT
│ ├─ AesGcmKeyManager
│ └─ SqliteProviderRepository(database.db_path)
│
▼
RookUsecases { route_request, manage_providers, health_check, authenticate_client_api, manage_connections }
│
▼
transport_axum::router(usecases, authz_config) ← axum Router with routes + state
Provider CRUD is an administrative storage and health-test surface in v1. SQLite provider connections are not hot-registered into the request router; TOML providers continue to serve completion traffic.
tracing + tracing-subscriber with env-filter. Structured JSON logs to stdout. Metrics via metrics crate (labels: provider, model, status).