use crate::emit::emit_ops_event;
use crate::governor::{Governor, GovernorError};
use crate::ops_event::OpsEvent;
use crate::tenant::current_tenant;
use crate::types::ProviderId;
use async_trait::async_trait;
use klieo_core::error::LlmError;
use klieo_core::ids::RunId;
use klieo_core::llm::{Capabilities, ChatRequest, ChatResponse, ChunkStream};
use klieo_core::memory::EpisodicMemory;
use klieo_core::LlmClient;
use std::sync::Arc;
pub struct GovernedLlmClient {
inner: Arc<dyn LlmClient>,
governor: Arc<dyn Governor>,
episodic: Arc<dyn EpisodicMemory>,
run_id: RunId,
provider: ProviderId,
}
impl GovernedLlmClient {
#[must_use]
pub fn new(
inner: Arc<dyn LlmClient>,
governor: Arc<dyn Governor>,
episodic: Arc<dyn EpisodicMemory>,
run_id: RunId,
provider: ProviderId,
) -> Self {
Self {
inner,
governor,
episodic,
run_id,
provider,
}
}
async fn acquire_permit(&self) -> Result<crate::governor::Permit, LlmError> {
self.governor
.acquire_llm(self.provider.clone(), 4096)
.await
.map_err(|e| self.translate_governor_error(e))
}
fn translate_governor_error(&self, e: GovernorError) -> LlmError {
match e {
GovernorError::Saturated { .. } => LlmError::RateLimit {
retry_after_secs: 1,
},
GovernorError::TimedOut { .. } => LlmError::Timeout,
GovernorError::Unavailable(s) => LlmError::Server(s),
}
}
async fn record_denial(&self, reason: String) {
if let Err(err) = emit_ops_event(
&*self.episodic,
self.run_id,
OpsEvent::GovernorDenial {
tenant: current_tenant(),
resource: "llm".into(),
provider: Some(self.provider.clone()),
host: None,
reason,
},
)
.await
{
tracing::warn!(
target: "klieo.ops.audit",
error = %err,
"audit emit failed; episode not recorded"
);
}
}
}
#[async_trait]
impl LlmClient for GovernedLlmClient {
fn name(&self) -> &str {
self.inner.name()
}
fn capabilities(&self) -> &Capabilities {
self.inner.capabilities()
}
async fn complete(&self, req: ChatRequest) -> Result<ChatResponse, LlmError> {
match self.acquire_permit().await {
Ok(_permit) => self.inner.complete(req).await,
Err(e) => {
self.record_denial(format!("{e}")).await;
Err(e)
}
}
}
async fn stream(&self, req: ChatRequest) -> Result<ChunkStream, LlmError> {
match self.acquire_permit().await {
Ok(_permit) => self.inner.stream(req).await,
Err(e) => {
self.record_denial(format!("{e}")).await;
Err(e)
}
}
}
async fn embed(&self, texts: &[String]) -> Result<Vec<klieo_core::llm::Embedding>, LlmError> {
match self.acquire_permit().await {
Ok(_permit) => self.inner.embed(texts).await,
Err(e) => {
self.record_denial(format!("{e}")).await;
Err(e)
}
}
}
}