use async_trait::async_trait;
use futures::stream::BoxStream;
use tracing::field::Empty;
use tracing::Instrument;
use crate::audio::{
AudioResponse, SpeechToTextRequest, TextToSpeechRequest, TranscriptionResponse,
};
use crate::embeddings::{EmbeddingsRequest, EmbeddingsResponse};
use crate::error::Error;
use crate::images::{ImagesRequest, ImagesResponse};
use crate::middleware::ProviderMiddleware;
use crate::moderation::{ModerationRequest, ModerationResponse};
use crate::provider::Provider;
use crate::rerank::{RerankRequest, RerankResponse};
use crate::stream_event::StreamEvent;
use crate::structured::{StructuredRequest, StructuredResponse};
use crate::text::{Step, TextRequest};
#[derive(Debug, Clone, Copy, Default)]
pub struct GenAiTracingMiddleware;
#[async_trait]
impl ProviderMiddleware for GenAiTracingMiddleware {
async fn text_step(&self, request: TextRequest, next: &dyn Provider) -> Result<Step, Error> {
let span = tracing::info_span!(
"gen_ai.chat",
"gen_ai.operation.name" = "chat",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.request.stream" = false,
"gen_ai.request.temperature" = ?request.temperature,
"gen_ai.request.max_tokens" = ?request.max_tokens,
"gen_ai.request.top_p" = ?request.top_p,
"gen_ai.response.model" = Empty,
"gen_ai.usage.input_tokens" = Empty,
"gen_ai.usage.output_tokens" = Empty,
);
async move {
let result = next.text_step(&request).await;
if let Ok(step) = &result {
record_response(&step.meta, Some(&step.usage));
}
result
}
.instrument(span)
.await
}
async fn stream_text_once(
&self,
request: TextRequest,
next: &dyn Provider,
) -> Result<BoxStream<'static, Result<StreamEvent, Error>>, Error> {
let span = tracing::info_span!(
"gen_ai.chat",
"gen_ai.operation.name" = "chat",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.request.stream" = true,
"gen_ai.request.temperature" = ?request.temperature,
"gen_ai.request.max_tokens" = ?request.max_tokens,
"gen_ai.request.top_p" = ?request.top_p,
);
async move { next.stream_text_once(&request).await }
.instrument(span)
.await
}
async fn structured(
&self,
request: StructuredRequest,
next: &dyn Provider,
) -> Result<StructuredResponse, Error> {
let span = tracing::info_span!(
"gen_ai.structured_output",
"gen_ai.operation.name" = "structured_output",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.request.temperature" = ?request.temperature,
"gen_ai.request.max_tokens" = ?request.max_tokens,
"gen_ai.request.top_p" = ?request.top_p,
"gen_ai.response.model" = Empty,
"gen_ai.usage.input_tokens" = Empty,
"gen_ai.usage.output_tokens" = Empty,
);
async move {
let result = next.structured(request).await;
if let Ok(response) = &result {
record_response(&response.meta, Some(&response.usage));
}
result
}
.instrument(span)
.await
}
async fn moderation(
&self,
request: ModerationRequest,
next: &dyn Provider,
) -> Result<ModerationResponse, Error> {
let span = tracing::info_span!(
"gen_ai.moderation",
"gen_ai.operation.name" = "moderation",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
);
async move {
let result = next.moderation(request).await;
if let Ok(response) = &result {
record_response(&response.meta, None);
}
result
}
.instrument(span)
.await
}
async fn embeddings(
&self,
request: EmbeddingsRequest,
next: &dyn Provider,
) -> Result<EmbeddingsResponse, Error> {
let span = tracing::info_span!(
"gen_ai.embeddings",
"gen_ai.operation.name" = "embeddings",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
"gen_ai.usage.input_tokens" = Empty,
);
async move {
let result = next.embeddings(request).await;
if let Ok(response) = &result {
record_response_model(&response.meta);
tracing::Span::current().record(
"gen_ai.usage.input_tokens",
response.usage.prompt_tokens as u64,
);
}
result
}
.instrument(span)
.await
}
async fn rerank(
&self,
request: RerankRequest,
next: &dyn Provider,
) -> Result<RerankResponse, Error> {
let span = tracing::info_span!(
"gen_ai.rerank",
"gen_ai.operation.name" = "rerank",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
"gen_ai.usage.input_tokens" = Empty,
);
async move {
let result = next.rerank(request).await;
if let Ok(response) = &result {
record_response_model(&response.meta);
tracing::Span::current().record(
"gen_ai.usage.input_tokens",
response.usage.prompt_tokens as u64,
);
}
result
}
.instrument(span)
.await
}
async fn images(
&self,
request: ImagesRequest,
next: &dyn Provider,
) -> Result<ImagesResponse, Error> {
let span = tracing::info_span!(
"gen_ai.image_generation",
"gen_ai.operation.name" = "image_generation",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
);
async move {
let result = next.images(request).await;
if let Ok(response) = &result {
record_response_model(&response.meta);
}
result
}
.instrument(span)
.await
}
async fn text_to_speech(
&self,
request: TextToSpeechRequest,
next: &dyn Provider,
) -> Result<AudioResponse, Error> {
let span = tracing::info_span!(
"gen_ai.text_to_speech",
"gen_ai.operation.name" = "text_to_speech",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
);
async move {
let result = next.text_to_speech(request).await;
if let Ok(response) = &result {
record_response_model(&response.meta);
}
result
}
.instrument(span)
.await
}
async fn speech_to_text(
&self,
request: SpeechToTextRequest,
next: &dyn Provider,
) -> Result<TranscriptionResponse, Error> {
let span = tracing::info_span!(
"gen_ai.speech_to_text",
"gen_ai.operation.name" = "speech_to_text",
"gen_ai.provider.name" = next.name(),
"gen_ai.request.model" = %request.model,
"gen_ai.response.model" = Empty,
);
async move {
let result = next.speech_to_text(request).await;
if let Ok(response) = &result {
record_response_model(&response.meta);
}
result
}
.instrument(span)
.await
}
}
fn record_response(meta: &crate::value_objects::Meta, usage: Option<&crate::value_objects::Usage>) {
record_response_model(meta);
if let Some(usage) = usage {
let span = tracing::Span::current();
span.record("gen_ai.usage.input_tokens", usage.prompt_tokens as u64);
span.record("gen_ai.usage.output_tokens", usage.completion_tokens as u64);
}
}
fn record_response_model(meta: &crate::value_objects::Meta) {
tracing::Span::current().record("gen_ai.response.model", meta.model.as_deref());
}