use crate::metrics::names::API_CALL_COUNT_METRIC;use crate::metrics::names::API_CALL_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_INFERENCE_TIME_DURATION_METRIC;use crate::metrics::names::RESPONSES_API_OVERHEAD_DURATION_METRIC;use crate::metrics::names::SSE_EVENT_COUNT_METRIC;use crate::metrics::names::SSE_EVENT_DURATION_METRIC;use crate::metrics::names::TOOL_CALL_COUNT_METRIC;use crate::metrics::names::TOOL_CALL_DURATION_METRIC;use crate::metrics::names::TURN_TTFM_DURATION_METRIC;use crate::metrics::names::TURN_TTFT_DURATION_METRIC;use crate::metrics::names::WEBSOCKET_EVENT_COUNT_METRIC;use crate::metrics::names::WEBSOCKET_EVENT_DURATION_METRIC;use crate::metrics::names::WEBSOCKET_REQUEST_COUNT_METRIC;use crate::metrics::names::WEBSOCKET_REQUEST_DURATION_METRIC;use opentelemetry_sdk::metrics::data::AggregatedMetrics;use opentelemetry_sdk::metrics::data::Metric;use opentelemetry_sdk::metrics::data::MetricData;use opentelemetry_sdk::metrics::data::ResourceMetrics;#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]pub struct RuntimeMetricTotals { pub count: u64, pub duration_ms: u64,}impl RuntimeMetricTotals { pub fn is_empty(self) -> bool { self.count == 0 && self.duration_ms == 0 } pub fn merge(&mut self, other: Self) { self.count = self.count.saturating_add(other.count); self.duration_ms = self.duration_ms.saturating_add(other.duration_ms); }}#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]pub struct RuntimeMetricsSummary { pub tool_calls: RuntimeMetricTotals, pub api_calls: RuntimeMetricTotals, pub streaming_events: RuntimeMetricTotals, pub websocket_calls: RuntimeMetricTotals, pub websocket_events: RuntimeMetricTotals, pub responses_api_overhead_ms: u64, pub responses_api_inference_time_ms: u64, pub responses_api_engine_iapi_ttft_ms: u64, pub responses_api_engine_service_ttft_ms: u64, pub responses_api_engine_iapi_tbt_ms: u64, pub responses_api_engine_service_tbt_ms: u64, pub turn_ttft_ms: u64, pub turn_ttfm_ms: u64,}impl RuntimeMetricsSummary { pub fn is_empty(self) -> bool { self.tool_calls.is_empty() && self.api_calls.is_empty() && self.streaming_events.is_empty() && self.websocket_calls.is_empty() && self.websocket_events.is_empty() && self.responses_api_overhead_ms == 0 && self.responses_api_inference_time_ms == 0 && self.responses_api_engine_iapi_ttft_ms == 0 && self.responses_api_engine_service_ttft_ms == 0 && self.responses_api_engine_iapi_tbt_ms == 0 && self.responses_api_engine_service_tbt_ms == 0 && self.turn_ttft_ms == 0 && self.turn_ttfm_ms == 0 } pub fn merge(&mut self, other: Self) { self.tool_calls.merge(other.tool_calls); self.api_calls.merge(other.api_calls); self.streaming_events.merge(other.streaming_events); self.websocket_calls.merge(other.websocket_calls); self.websocket_events.merge(other.websocket_events); if other.responses_api_overhead_ms > 0 { self.responses_api_overhead_ms = other.responses_api_overhead_ms; } if other.responses_api_inference_time_ms > 0 { self.responses_api_inference_time_ms = other.responses_api_inference_time_ms; } if other.responses_api_engine_iapi_ttft_ms > 0 { self.responses_api_engine_iapi_ttft_ms = other.responses_api_engine_iapi_ttft_ms; } if other.responses_api_engine_service_ttft_ms > 0 { self.responses_api_engine_service_ttft_ms = other.responses_api_engine_service_ttft_ms; } if other.responses_api_engine_iapi_tbt_ms > 0 { self.responses_api_engine_iapi_tbt_ms = other.responses_api_engine_iapi_tbt_ms; } if other.responses_api_engine_service_tbt_ms > 0 { self.responses_api_engine_service_tbt_ms = other.responses_api_engine_service_tbt_ms; } if other.turn_ttft_ms > 0 { self.turn_ttft_ms = other.turn_ttft_ms; } if other.turn_ttfm_ms > 0 { self.turn_ttfm_ms = other.turn_ttfm_ms; } } pub fn responses_api_summary(&self) -> RuntimeMetricsSummary { Self { responses_api_overhead_ms: self.responses_api_overhead_ms, responses_api_inference_time_ms: self.responses_api_inference_time_ms, responses_api_engine_iapi_ttft_ms: self.responses_api_engine_iapi_ttft_ms, responses_api_engine_service_ttft_ms: self.responses_api_engine_service_ttft_ms, responses_api_engine_iapi_tbt_ms: self.responses_api_engine_iapi_tbt_ms, responses_api_engine_service_tbt_ms: self.responses_api_engine_service_tbt_ms, ..RuntimeMetricsSummary::default() } } pub(crate) fn from_snapshot(snapshot: &ResourceMetrics) -> Self { let tool_calls = RuntimeMetricTotals { count: sum_counter(snapshot, TOOL_CALL_COUNT_METRIC), duration_ms: sum_histogram_ms(snapshot, TOOL_CALL_DURATION_METRIC), }; let api_calls = RuntimeMetricTotals { count: sum_counter(snapshot, API_CALL_COUNT_METRIC), duration_ms: sum_histogram_ms(snapshot, API_CALL_DURATION_METRIC), }; let streaming_events = RuntimeMetricTotals { count: sum_counter(snapshot, SSE_EVENT_COUNT_METRIC), duration_ms: sum_histogram_ms(snapshot, SSE_EVENT_DURATION_METRIC), }; let websocket_calls = RuntimeMetricTotals { count: sum_counter(snapshot, WEBSOCKET_REQUEST_COUNT_METRIC), duration_ms: sum_histogram_ms(snapshot, WEBSOCKET_REQUEST_DURATION_METRIC), }; let websocket_events = RuntimeMetricTotals { count: sum_counter(snapshot, WEBSOCKET_EVENT_COUNT_METRIC), duration_ms: sum_histogram_ms(snapshot, WEBSOCKET_EVENT_DURATION_METRIC), }; let responses_api_overhead_ms = sum_histogram_ms(snapshot, RESPONSES_API_OVERHEAD_DURATION_METRIC); let responses_api_inference_time_ms = sum_histogram_ms(snapshot, RESPONSES_API_INFERENCE_TIME_DURATION_METRIC); let responses_api_engine_iapi_ttft_ms = sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_IAPI_TTFT_DURATION_METRIC); let responses_api_engine_service_ttft_ms = sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_SERVICE_TTFT_DURATION_METRIC); let responses_api_engine_iapi_tbt_ms = sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_IAPI_TBT_DURATION_METRIC); let responses_api_engine_service_tbt_ms = sum_histogram_ms(snapshot, RESPONSES_API_ENGINE_SERVICE_TBT_DURATION_METRIC); let turn_ttft_ms = sum_histogram_ms(snapshot, TURN_TTFT_DURATION_METRIC); let turn_ttfm_ms = sum_histogram_ms(snapshot, TURN_TTFM_DURATION_METRIC); Self { tool_calls, api_calls, streaming_events, websocket_calls, websocket_events, responses_api_overhead_ms, responses_api_inference_time_ms, responses_api_engine_iapi_ttft_ms, responses_api_engine_service_ttft_ms, responses_api_engine_iapi_tbt_ms, responses_api_engine_service_tbt_ms, turn_ttft_ms, turn_ttfm_ms, } }}fn sum_counter(snapshot: &ResourceMetrics, name: &str) -> u64 { snapshot .scope_metrics() .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) .filter(|metric| metric.name() == name) .map(sum_counter_metric) .sum()}fn sum_counter_metric(metric: &Metric) -> u64 { match metric.data() { AggregatedMetrics::U64(MetricData::Sum(sum)) => sum .data_points() .map(opentelemetry_sdk::metrics::data::SumDataPoint::value) .sum(), _ => 0, }}fn sum_histogram_ms(snapshot: &ResourceMetrics, name: &str) -> u64 { snapshot .scope_metrics() .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) .filter(|metric| metric.name() == name) .map(sum_histogram_metric_ms) .sum()}fn sum_histogram_metric_ms(metric: &Metric) -> u64 { match metric.data() { AggregatedMetrics::F64(MetricData::Histogram(histogram)) => histogram .data_points() .map(|point| f64_to_u64(point.sum())) .sum(), _ => 0, }}fn f64_to_u64(value: f64) -> u64 { if !value.is_finite() || value <= 0.0 { return 0; } let clamped = value.min(u64::MAX as f64); clamped.round() as u64}