llmkit-tower 0.1.0

Tower middleware (retry, rate limit, cost tracking, tracing) for llmkit-rs
Documentation
//! Tracing layer: a structured span per call with latency and token counts.

use std::sync::Arc;
use std::time::Instant;

use async_trait::async_trait;
use llmkit_core::{
    ChatRequest, ChatResponse, ChatStream, CostEstimate, EmbedRequest, EmbedResponse, LlmProvider,
    LlmResult,
};

use crate::layer::LlmLayer;

/// Emits a `tracing` span around each call with latency and usage.
#[derive(Debug, Clone, Copy, Default)]
pub struct TracingLayer;

impl TracingLayer {
    /// Construct the layer.
    pub fn new() -> Self {
        Self
    }
}

impl LlmLayer for TracingLayer {
    type Provider = Tracing;
    fn layer(self, inner: Arc<dyn LlmProvider>) -> Tracing {
        Tracing { inner }
    }
}

/// Provider produced by [`TracingLayer`].
pub struct Tracing {
    inner: Arc<dyn LlmProvider>,
}

#[async_trait]
impl LlmProvider for Tracing {
    async fn chat(&self, req: ChatRequest) -> LlmResult<ChatResponse> {
        let span = tracing::info_span!("llm.chat", provider = self.inner.name(), model = self.inner.model());
        let _e = span.enter();
        let start = Instant::now();

        let result = self.inner.chat(req).await;
        let latency_ms = start.elapsed().as_millis() as u64;

        match &result {
            Ok(resp) => tracing::info!(
                latency_ms,
                prompt_tokens = resp.usage.prompt,
                completion_tokens = resp.usage.completion,
                "chat completed"
            ),
            Err(e) => tracing::warn!(latency_ms, error = %e, "chat failed"),
        }
        result
    }

    async fn chat_stream(&self, req: ChatRequest) -> LlmResult<ChatStream> {
        let span = tracing::info_span!("llm.chat_stream", provider = self.inner.name(), model = self.inner.model());
        let _e = span.enter();
        tracing::info!("stream opened");
        self.inner.chat_stream(req).await
    }

    async fn embed(&self, req: EmbedRequest) -> LlmResult<EmbedResponse> {
        let span = tracing::info_span!("llm.embed", provider = self.inner.name());
        let _e = span.enter();
        let start = Instant::now();
        let result = self.inner.embed(req).await;
        let latency_ms = start.elapsed().as_millis() as u64;
        match &result {
            Ok(resp) => tracing::info!(latency_ms, count = resp.embeddings.len(), "embed completed"),
            Err(e) => tracing::warn!(latency_ms, error = %e, "embed failed"),
        }
        result
    }

    fn name(&self) -> &'static str {
        self.inner.name()
    }

    fn model(&self) -> &str {
        self.inner.model()
    }

    fn estimate_cost(&self, req: &ChatRequest) -> Option<CostEstimate> {
        self.inner.estimate_cost(req)
    }
}