use std::sync::Arc;
use async_trait::async_trait;
use bitrouter_sdk::PluginId;
use bitrouter_sdk::Result;
use bitrouter_sdk::language_model::{DenyReason, HookDecision, PipelineContext, PreRequestHook};
use crate::metering::{MeteringStore, TimeWindow};
use crate::policy::store::PolicyStore;
pub struct PolicyHook {
store: Arc<PolicyStore>,
metering: Option<MeteringStore>,
}
impl PolicyHook {
pub fn new(store: Arc<PolicyStore>, metering: Option<MeteringStore>) -> Self {
Self { store, metering }
}
fn policy_id(ctx: &PipelineContext) -> Option<String> {
ctx.get_metadata(&PluginId::new("bitrouter-auth"))
.and_then(|m| m.get("policy_id"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
}
#[async_trait]
impl PreRequestHook for PolicyHook {
async fn check(&self, ctx: &mut PipelineContext) -> Result<HookDecision> {
let policy_id = Self::policy_id(ctx);
let ids: Vec<&str> = policy_id.as_deref().into_iter().collect();
let effective = self.store.effective_for(&ids);
if let Err(violation) = effective.check_model(ctx.model()) {
return Ok(HookDecision::Deny(DenyReason::Forbidden(
violation.to_string(),
)));
}
if let Err(violation) = effective.check_expiry(chrono::Utc::now()) {
return Ok(HookDecision::Deny(DenyReason::Forbidden(
violation.to_string(),
)));
}
if effective.has_tool_restriction() {
let tools = ctx.prompt().tools.iter().map(|t| t.name());
if let Err(violation) = effective.check_tools(tools) {
return Ok(HookDecision::Deny(DenyReason::Forbidden(
violation.to_string(),
)));
}
}
if effective.max_spend_micro_usd.is_some()
&& let Some(metering) = &self.metering
{
let spent = metering
.get_spend(ctx.caller().api_key_id(), TimeWindow::ThisMonth)
.await?;
if let Err(violation) = effective.check_spend(spent) {
return Ok(HookDecision::Deny(DenyReason::Forbidden(
violation.to_string(),
)));
}
}
if effective.max_requests_per_minute.is_some()
&& let Some(metering) = &self.metering
{
let rate = metering.get_rate(ctx.caller().api_key_id()).await?;
let observed = rate.requests_per_minute.round().max(0.0) as u32;
if let Err(violation) = effective.check_rate(observed) {
tracing::debug!(%violation, "policy rate limit hit");
return Ok(HookDecision::Deny(DenyReason::RateLimited {
retry_after: Some(60),
}));
}
}
Ok(HookDecision::Allow)
}
}