//! Incoming protocol adapters. Each one builds its own axum routes and translates //! its protocol to/from the canonical model. Enabled via flags in `[protocols]`. pub mod anthropic_messages; pub mod mcp; pub mod native_passthrough; pub mod openai_chat; pub mod openai_completions; pub mod openai_embeddings; pub mod openai_models; use crate::state::SharedState; use axum::Router; /// Account resolved by the API-key middleware, consumed by protocol handlers /// for attribution (stats, budgets). Present only when keys are configured. #[derive(Clone)] pub struct AccountTag(pub String); /// API-key gate for the model-facing surface. Open mode (no keys configured) /// passes everything through — the admin console already warns about it. /// With keys: Bearer/x-api-key must match (401), per-key rate_per_min is /// enforced with a fixed one-minute window (429 + Retry-After), and a /// monthly budget, when set, turns into 429 insufficient_quota once the /// account's month-to-date spend crosses it. async fn api_auth( axum::extract::State(state): axum::extract::State, mut req: axum::extract::Request, next: axum::middleware::Next, ) -> axum::response::Response { use axum::response::IntoResponse; let live = state.live(); if live.cfg.api_keys.is_empty() { return next.run(req).await; } let provided = req .headers() .get(axum::http::header::AUTHORIZATION) .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")) .or_else(|| req.headers().get("x-api-key").and_then(|v| v.to_str().ok())) .map(str::trim) .unwrap_or(""); let Some(key) = live.cfg.api_keys.iter().find(|k| k.key == provided) else { return crate::error::GatewayError::Unauthorized("invalid or missing API key".into()) .into_response(); }; if key.rate_per_min > 0 && !state.rate.allow(&key.key, key.rate_per_min) { return crate::error::GatewayError::RateLimited { retry_after_secs: state.rate.retry_after_secs(), } .into_response(); } if let Some(budget) = key.monthly_budget_usd { if budget > 0.0 && state.stats.month_spend(&key.account) >= budget { return crate::error::GatewayError::BudgetExhausted { budget_usd: budget } .into_response(); } } req.extensions_mut().insert(AccountTag(key.account.clone())); next.run(req).await } /// Build the protocols router. Implemented adapters are **always** mounted; /// each handler checks the live `protocols.*` flag from config itself, so /// toggles in the admin panel take effect without a restart (hot switching). pub fn build_router(state: SharedState) -> Router { Router::new() .merge(openai_chat::routes()) .merge(openai_completions::routes()) .merge(openai_embeddings::routes()) .merge(openai_models::routes()) .merge(anthropic_messages::routes()) .merge(mcp::routes()) .merge(native_passthrough::routes()) .route_layer(axum::middleware::from_fn_with_state(state, api_auth)) }