1use std::future::Future;
5use std::sync::Arc;
6
7use chrono::{DateTime, TimeDelta, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use typed_builder::TypedBuilder;
11use uuid::Uuid;
12
13use crate::api::event::{
14 BaseEvent, CategoryProfile, DataSchema, Event, EventCategory, MarkEvent, PendingMarkSpec,
15};
16use crate::api::optimization::{
17 LlmOptimizationRecorder, finalize_optimization_summary, scope_llm_optimization_recorder,
18};
19#[cfg(test)]
20use crate::api::runtime::LlmCodecIdentity;
21use crate::api::runtime::NemoRelayContextState;
22use crate::api::runtime::global_context;
23use crate::api::runtime::state::contextualize_stream;
24use crate::api::runtime::subscriber_dispatcher::{
25 PendingPublication, dispatch_reserved_sanitized_event, dispatch_sanitized_event,
26 dispatch_transformed_event, register_pending_publication,
27};
28use crate::api::runtime::{
29 EventSubscriberFn, LlmCollectorFn, LlmExecutionNextFn, LlmFinalizerFn, LlmJsonStream,
30 LlmSanitizeRequestContext, LlmSanitizeResponseContext, LlmStreamExecutionNextFn,
31 MiddlewareContinuationContext, with_active_event_uuid,
32};
33use crate::api::runtime::{ScopeStackHandle, current_scope_stack};
34use crate::api::scope::event;
35use crate::api::scope::{EmitMarkEventParams, ScopeHandle};
36use crate::api::shared::{
37 ensure_runtime_owner, inject_dynamo_session_ids, metadata_with_otel_error,
38 metadata_with_otel_status, resolve_parent_uuid, run_request_intercepts_with_codec_and_recorder,
39 snapshot_event_sanitizers, snapshot_event_subscribers,
40};
41use crate::codec::request::{AnnotatedLlmRequest, Message};
42use crate::codec::response::{AnnotatedLlmResponse, attach_estimated_cost_for_provider};
43use crate::codec::traits::{LlmCodec, LlmResponseCodec};
44use crate::error::{FlowError, Result};
45use crate::json::Json;
46use crate::stream::LlmStreamWrapper;
47
48pub use nemo_relay_types::api::llm::{
49 LLM_REQUEST_INTERCEPT_OUTCOME_SCHEMA, LlmAttributes, LlmRequest, LlmRequestInterceptOutcome,
50};
51
52const OBSERVABILITY_CREDENTIAL_HEADERS: [&str; 7] = [
53 "authorization",
54 "proxy-authorization",
55 "cookie",
56 "x-api-key",
57 "api-key",
58 "anthropic-api-key",
59 "x-goog-api-key",
60];
61
62fn queue_sanitized_event_with_scope_stack(
63 event: Event,
64 subscribers: &[EventSubscriberFn],
65 scope_stack: &ScopeStackHandle,
66) -> bool {
67 let sanitizers = snapshot_event_sanitizers(&event, scope_stack).unwrap_or_default();
68 dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone())
69}
70
71#[derive(Clone)]
72struct CapturedLlmScopeStack(ScopeStackHandle);
73
74impl Default for CapturedLlmScopeStack {
75 fn default() -> Self {
76 Self(current_scope_stack())
77 }
78}
79
80impl std::fmt::Debug for CapturedLlmScopeStack {
81 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 formatter.write_str("CapturedLlmScopeStack(..)")
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
88#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
89pub struct LlmHandle {
90 #[builder(default = Uuid::now_v7())]
92 pub uuid: Uuid,
93 #[builder(default = Utc::now())]
95 pub started_at: DateTime<Utc>,
96 #[builder(setter(into))]
102 pub name: String,
103 #[builder(default)]
105 pub data: Option<Json>,
106 #[builder(default)]
108 pub metadata: Option<Json>,
109 #[builder(default = LlmAttributes::empty())]
111 pub attributes: LlmAttributes,
112 #[builder(default)]
114 pub parent_uuid: Option<Uuid>,
115 #[builder(default, setter(into))]
117 pub model_name: Option<String>,
118 #[serde(skip, default)]
120 #[builder(default)]
121 pub optimization_recorder: LlmOptimizationRecorder,
122 #[serde(skip, default)]
127 #[builder(setter(skip), default)]
128 captured_scope_stack: CapturedLlmScopeStack,
129}
130
131impl LlmHandle {
132 pub(crate) fn captured_scope_stack(&self) -> &ScopeStackHandle {
133 &self.captured_scope_stack.0
134 }
135}
136
137#[derive(Debug, Clone, TypedBuilder)]
139#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
140pub struct CreateLlmHandleParams<'a> {
141 pub name: &'a str,
144 #[builder(default)]
146 pub parent_uuid: Option<uuid::Uuid>,
147 #[builder(default = LlmAttributes::empty())]
149 pub attributes: LlmAttributes,
150 #[builder(default)]
152 pub data: Option<Json>,
153 #[builder(default)]
155 pub metadata: Option<Json>,
156 #[builder(default, setter(into))]
158 pub model_name: Option<String>,
159 #[builder(default)]
162 pub timestamp: Option<DateTime<Utc>>,
163}
164
165#[derive(Clone, TypedBuilder)]
167#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
168pub struct EndLlmHandleParams<'a> {
169 pub handle: &'a LlmHandle,
171 #[builder(default)]
173 pub data: Option<Json>,
174 #[builder(default)]
176 pub metadata: Option<Json>,
177 #[builder(default)]
179 pub annotated_response: Option<Arc<AnnotatedLlmResponse>>,
180 #[builder(default)]
184 pub timestamp: Option<DateTime<Utc>>,
185}
186
187#[derive(TypedBuilder)]
189#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
190pub struct LlmCallParams<'a> {
191 pub name: &'a str,
193 pub request: &'a LlmRequest,
195 #[builder(default)]
197 pub parent: Option<&'a ScopeHandle>,
198 #[builder(default = LlmAttributes::empty())]
200 pub attributes: LlmAttributes,
201 #[builder(default)]
204 pub data: Option<Json>,
205 #[builder(default)]
207 pub metadata: Option<Json>,
208 #[builder(default, setter(into))]
210 pub model_name: Option<String>,
211 #[builder(default)]
213 pub annotated_request: Option<Arc<AnnotatedLlmRequest>>,
214 #[builder(default)]
217 pub timestamp: Option<DateTime<Utc>>,
218}
219
220#[derive(TypedBuilder)]
222#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
223pub struct LlmCallExecuteParams {
224 #[builder(setter(into))]
226 pub name: String,
227 pub request: LlmRequest,
229 pub func: LlmExecutionNextFn,
231 #[builder(default)]
233 pub parent: Option<ScopeHandle>,
234 #[builder(default = LlmAttributes::empty())]
236 pub attributes: LlmAttributes,
237 #[builder(default)]
240 pub data: Option<Json>,
241 #[builder(default)]
243 pub metadata: Option<Json>,
244 #[builder(default, setter(into))]
246 pub model_name: Option<String>,
247 #[builder(default)]
249 pub codec: Option<Arc<dyn LlmCodec>>,
250 #[builder(default)]
252 pub response_codec: Option<Arc<dyn LlmResponseCodec>>,
253}
254
255#[derive(TypedBuilder)]
257#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
258pub struct LlmStreamCallExecuteParams {
259 #[builder(setter(into))]
261 pub name: String,
262 pub request: LlmRequest,
264 pub func: LlmStreamExecutionNextFn,
266 pub collector: LlmCollectorFn,
268 pub finalizer: LlmFinalizerFn,
270 #[builder(default)]
272 pub parent: Option<ScopeHandle>,
273 #[builder(default = LlmAttributes::empty())]
275 pub attributes: LlmAttributes,
276 #[builder(default)]
279 pub data: Option<Json>,
280 #[builder(default)]
282 pub metadata: Option<Json>,
283 #[builder(default, setter(into))]
285 pub model_name: Option<String>,
286 #[builder(default)]
288 pub codec: Option<Arc<dyn LlmCodec>>,
289 #[builder(default)]
291 pub response_codec: Option<Arc<dyn LlmResponseCodec>>,
292}
293
294#[derive(TypedBuilder)]
296#[builder(field_defaults(setter(strip_option(ignore_invalid, fallback_suffix = "_opt"))))]
297pub struct LlmCallEndParams<'a> {
298 pub handle: &'a LlmHandle,
300 pub response: Json,
302 #[builder(default)]
305 pub data: Option<Json>,
306 #[builder(default)]
308 pub metadata: Option<Json>,
309 #[builder(default)]
311 pub annotated_response: Option<Arc<AnnotatedLlmResponse>>,
312 #[builder(default)]
314 pub response_codec: Option<Arc<dyn LlmResponseCodec>>,
315 #[builder(default)]
319 pub timestamp: Option<DateTime<Utc>>,
320}
321
322fn create_llm_handle(params: CreateLlmHandleParams<'_>) -> Result<LlmHandle> {
323 ensure_runtime_owner()?;
324 let context = global_context();
325 let state = context
326 .read()
327 .map_err(|error| FlowError::Internal(error.to_string()))?;
328 Ok(state.create_llm_handle(params))
329}
330
331fn request_turn_projection_needed<T>(
332 items: &[T],
333 is_user: &impl Fn(&T) -> bool,
334 is_instruction: &impl Fn(&T) -> bool,
335) -> bool {
336 let Some(last_index) = items.len().checked_sub(1) else {
337 return false;
338 };
339 match items.iter().rposition(is_user) {
340 Some(start) => items[..start].iter().any(|item| !is_instruction(item)),
341 None => items
342 .iter()
343 .enumerate()
344 .any(|(index, item)| index != last_index && !is_instruction(item)),
345 }
346}
347
348fn retain_current_request_turn<T>(
349 items: &mut Vec<T>,
350 is_user: impl Fn(&T) -> bool,
351 is_instruction: impl Fn(&T) -> bool,
352) -> bool {
353 if !request_turn_projection_needed(items, &is_user, &is_instruction) {
354 return false;
355 }
356 let last_index = items.len() - 1;
357 let Some(start) = items.iter().rposition(is_user) else {
358 let mut index = 0;
359 items.retain(|item| {
360 let retain = index == last_index || is_instruction(item);
361 index += 1;
362 retain
363 });
364 return true;
365 };
366 let mut current_turn = items.split_off(start);
367 items.retain(is_instruction);
368 items.append(&mut current_turn);
369 true
370}
371
372fn project_llm_request_to_current_user_turn(
373 request: &mut LlmRequest,
374 annotated_request: &mut Option<Arc<AnnotatedLlmRequest>>,
375 request_codec: Option<&dyn LlmCodec>,
376) {
377 let Some(annotation) = annotated_request.as_mut() else {
378 return;
379 };
380 if !request_turn_projection_needed(
381 &annotation.messages,
382 &|message| matches!(message, Message::User { .. }),
383 &|message| matches!(message, Message::System { .. }),
384 ) {
385 return;
386 }
387 let original_annotation = request_codec.map(|_| Arc::clone(annotation));
388 let projected = limit_annotated_request_history_to_current_user_turn(Arc::make_mut(annotation));
389 debug_assert!(projected);
390 if let Some(codec) = request_codec {
391 match codec.encode(annotation, request) {
392 Ok(encoded) => *request = encoded,
393 Err(_) => {
394 log::warn!(
395 target: "nemo_relay.observability",
396 event = "projection_failed",
397 projection = "llm_current_turn",
398 recovery = "preserve_full_history";
399 "LLM request projection failed; preserving full event history"
400 );
401 *annotation = original_annotation
402 .expect("codec-backed projection should preserve the original annotation")
403 }
404 }
405 }
406}
407
408fn limit_annotated_request_history_to_current_user_turn(
409 annotated_request: &mut AnnotatedLlmRequest,
410) -> bool {
411 retain_current_request_turn(
412 &mut annotated_request.messages,
413 |message| matches!(message, Message::User { .. }),
414 |message| matches!(message, Message::System { .. }),
415 )
416}
417
418async fn emit_llm_start_with_subscribers(
419 handle: &LlmHandle,
420 request: &LlmRequest,
421 annotated_request: Option<Arc<AnnotatedLlmRequest>>,
422 request_codec: Option<Arc<dyn LlmCodec>>,
423 subscribers: &[EventSubscriberFn],
424) -> Result<()> {
425 ensure_runtime_owner()?;
426 let (entries, full_payloads_enabled) = {
427 let scope_stack = handle.captured_scope_stack();
428 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
429 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
430 ®istries.llm_sanitize_request_guardrails
431 });
432 let context = global_context();
433 let state = context
434 .read()
435 .map_err(|error| FlowError::Internal(error.to_string()))?;
436 (
437 state.llm_sanitize_request_entries(&scope_locals),
438 state.observability_full_payloads_enabled,
439 )
440 };
441 let observable_request = remove_observability_credential_headers(request.clone());
442 let mut sanitized_request = NemoRelayContextState::llm_sanitize_request_snapshot_chain(
443 observable_request.clone(),
444 LlmSanitizeRequestContext::for_request_codec(request_codec.clone()),
445 &entries,
446 )
447 .await;
448 let request_changed = sanitized_request
449 .as_ref()
450 .is_some_and(|sanitized_request| sanitized_request != &observable_request);
451 let mut annotated_request = match (sanitized_request.as_ref(), request_codec.as_deref()) {
452 (Some(sanitized_request), Some(codec)) if request_changed => {
453 codec.decode(sanitized_request).ok().map(Arc::new)
454 }
455 (Some(_), _) if !request_changed => annotated_request,
456 (None, _) => None,
457 (Some(_), _) => None,
458 };
459 let scope_stack = handle.captured_scope_stack();
460 let agent_is_fresh = {
461 let mut scope_guard = scope_stack.write().expect("scope stack lock poisoned");
462 scope_guard.take_agent_freshness(handle.parent_uuid)
463 };
464 if !full_payloads_enabled
465 && !agent_is_fresh
466 && let Some(sanitized_request) = sanitized_request.as_mut()
467 {
468 project_llm_request_to_current_user_turn(
469 sanitized_request,
470 &mut annotated_request,
471 request_codec.as_deref(),
472 );
473 }
474 let input = sanitized_request
475 .as_ref()
476 .and_then(|sanitized_request| serde_json::to_value(sanitized_request).ok());
477 let event = {
478 let context = global_context();
479 let state = context
480 .read()
481 .map_err(|error| FlowError::Internal(error.to_string()))?;
482 state.build_llm_start_event(handle, input, annotated_request)
483 };
484 queue_sanitized_event_with_scope_stack(event, subscribers, scope_stack);
485 Ok(())
486}
487
488fn remove_observability_credential_headers(mut request: LlmRequest) -> LlmRequest {
489 request.headers.retain(|name, _| {
490 !OBSERVABILITY_CREDENTIAL_HEADERS
491 .iter()
492 .any(|credential_header| name.eq_ignore_ascii_case(credential_header))
493 });
494 request
495}
496
497#[cfg(test)]
501fn emit_llm_start(
502 handle: &LlmHandle,
503 request: &LlmRequest,
504 annotated_request: Option<Arc<AnnotatedLlmRequest>>,
505 request_codec: Option<Arc<dyn LlmCodec>>,
506) -> Result<()> {
507 let subscribers = {
508 let scope_stack = handle.captured_scope_stack();
509 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
510 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
511 };
512 crate::api::runtime::subscriber_dispatcher::block_on_sanitizer_future(
513 emit_llm_start_with_subscribers(
514 handle,
515 request,
516 annotated_request,
517 request_codec,
518 &subscribers,
519 ),
520 )
521 .map_err(FlowError::Internal)?
522}
523
524async fn emit_pending_request_marks(
525 handle: &LlmHandle,
526 marks: Vec<PendingMarkSpec>,
527 subscribers: &[EventSubscriberFn],
528) -> Result<()> {
529 if marks.is_empty() {
530 return Ok(());
531 }
532 ensure_runtime_owner()?;
533 let timestamp = handle.started_at + TimeDelta::microseconds(1);
534 for mark in marks {
535 let event = Event::Mark(MarkEvent::new(
536 BaseEvent::builder()
537 .name(mark.name)
538 .parent_uuid(handle.uuid)
539 .timestamp(timestamp)
540 .data_opt(mark.data)
541 .metadata_opt(mark.metadata)
542 .build(),
543 mark.category,
544 mark.category_profile,
545 ));
546 queue_sanitized_event_with_scope_stack(event, subscribers, handle.captured_scope_stack());
547 }
548 Ok(())
549}
550
551pub(crate) async fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) {
552 emit_optimization_marks_with_async(
553 handle,
554 subscribers,
555 |event| async { Some(event) },
556 |event, subscribers| {
557 queue_sanitized_event_with_scope_stack(
558 event.clone(),
559 subscribers,
560 handle.captured_scope_stack(),
561 )
562 },
563 )
564 .await;
565}
566
567pub(crate) async fn emit_reserved_optimization_marks(
568 handle: &LlmHandle,
569 subscribers: &[EventSubscriberFn],
570) {
571 emit_optimization_marks_with_async(
572 handle,
573 subscribers,
574 |event| async { Some(event) },
575 |event, subscribers| {
576 let sanitizers =
577 snapshot_event_sanitizers(event, handle.captured_scope_stack()).unwrap_or_default();
578 dispatch_reserved_sanitized_event(
579 event.clone(),
580 sanitizers,
581 subscribers,
582 handle.captured_scope_stack().clone(),
583 )
584 },
585 )
586 .await;
587}
588
589fn enqueue_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) {
595 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
596 if contributions.is_empty() || ensure_runtime_owner().is_err() {
597 return;
598 }
599 let scope_stack = handle.captured_scope_stack().clone();
600 for (contribution, recorded_at) in contributions {
601 let event = optimization_mark_event(handle, &contribution, recorded_at);
602 let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
603 if dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone()) {
604 handle.optimization_recorder.mark_emitted(1);
605 } else {
606 break;
607 }
608 }
609}
610
611async fn emit_optimization_marks_with_async<F, Fut>(
612 handle: &LlmHandle,
613 subscribers: &[EventSubscriberFn],
614 mut sanitize: F,
615 mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool,
616) where
617 F: FnMut(Event) -> Fut,
618 Fut: Future<Output = Option<Event>>,
619{
620 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
621 if contributions.is_empty() {
622 return;
623 }
624 if ensure_runtime_owner().is_err() {
625 log::warn!(
626 target: "nemo_relay.observability",
627 event = "optimization_marks_skipped",
628 reason = "runtime_owner_unavailable",
629 contribution_count = contributions.len();
630 "LLM optimization marks were skipped"
631 );
632 return;
633 }
634 for (contribution, recorded_at) in contributions {
635 let event = optimization_mark_event(handle, &contribution, recorded_at);
636 let Some(event) = sanitize(event).await else {
637 break;
641 };
642 if enqueue(&event, subscribers) {
643 handle.optimization_recorder.mark_emitted(1);
644 } else {
645 break;
649 }
650 }
651}
652
653fn optimization_mark_event(
654 handle: &LlmHandle,
655 contribution: &crate::codec::optimization::LlmOptimizationContribution,
656 recorded_at: DateTime<Utc>,
657) -> Event {
658 let offset = contribution.sequence.unwrap_or(0).saturating_add(2);
659 let offset = i64::try_from(offset).unwrap_or(i64::MAX);
660 let request_ordered_timestamp = handle.started_at + TimeDelta::microseconds(offset);
661 Event::Mark(MarkEvent::new(
662 BaseEvent::builder()
663 .name("nemo_relay.llm.optimization")
664 .parent_uuid(handle.uuid)
665 .timestamp(recorded_at.max(request_ordered_timestamp))
666 .data(serde_json::to_value(contribution).unwrap_or(Json::Null))
667 .data_schema(DataSchema {
668 name: "nemo.relay.llm_optimization_contribution".to_string(),
669 version: "1".to_string(),
670 })
671 .build(),
672 Some(EventCategory::custom()),
673 Some(
674 CategoryProfile::builder()
675 .subtype("nemo_relay.llm.optimization")
676 .build(),
677 ),
678 ))
679}
680
681#[cfg(test)]
685fn emit_optimization_marks_with<F>(
686 handle: &LlmHandle,
687 subscribers: &[EventSubscriberFn],
688 mut sanitize: F,
689 mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool,
690) where
691 F: FnMut(Event) -> Option<Event>,
692{
693 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
694 if contributions.is_empty() || ensure_runtime_owner().is_err() {
695 return;
696 }
697 for (contribution, recorded_at) in contributions {
698 let event = optimization_mark_event(handle, &contribution, recorded_at);
699 let Some(event) = sanitize(event) else {
700 break;
701 };
702 if enqueue(&event, subscribers) {
703 handle.optimization_recorder.mark_emitted(1);
704 } else {
705 break;
706 }
707 }
708}
709
710pub fn llm_call(params: LlmCallParams<'_>) -> Result<LlmHandle> {
753 let handle_params = CreateLlmHandleParams::builder()
754 .name(params.name)
755 .parent_uuid_opt(resolve_parent_uuid(params.parent))
756 .attributes(params.attributes)
757 .data_opt(params.data)
758 .metadata_opt(params.metadata)
759 .model_name_opt(params.model_name)
760 .timestamp_opt(params.timestamp)
761 .build();
762 let handle = create_llm_handle(handle_params)?;
763 let scope_stack = handle.captured_scope_stack().clone();
764 let (entries, subscribers, agent_is_fresh, full_payloads_enabled) = {
765 let mut scope_guard = scope_stack
766 .write()
767 .map_err(|error| FlowError::Internal(error.to_string()))?;
768 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
769 ®istries.llm_sanitize_request_guardrails
770 });
771 let subscribers =
772 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
773 let context = global_context();
774 let state = context
775 .read()
776 .map_err(|error| FlowError::Internal(error.to_string()))?;
777 let entries = state.llm_sanitize_request_entries(&scope_locals);
778 let full_payloads_enabled = state.observability_full_payloads_enabled;
779 drop(state);
780 let agent_is_fresh = scope_guard.take_agent_freshness(handle.parent_uuid);
781 (entries, subscribers, agent_is_fresh, full_payloads_enabled)
782 };
783 let request = remove_observability_credential_headers(params.request.clone());
786 let annotated_request = params.annotated_request;
787 let event = {
788 let context = global_context();
789 let state = context
790 .read()
791 .map_err(|error| FlowError::Internal(error.to_string()))?;
792 state.build_llm_start_event(&handle, None, None)
793 };
794 let queued_handle = handle.clone();
795 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
796 dispatch_transformed_event(
797 event,
798 Box::new(move |event| {
799 Box::pin(async move {
800 let mut sanitized_request =
801 NemoRelayContextState::llm_sanitize_request_snapshot_chain(
802 request.clone(),
803 LlmSanitizeRequestContext::default(),
804 &entries,
805 )
806 .await;
807 let request_changed = sanitized_request
808 .as_ref()
809 .is_some_and(|sanitized| sanitized != &request);
810 let mut annotation = if sanitized_request.is_none() || request_changed {
811 None
812 } else {
813 annotated_request
814 };
815 if !full_payloads_enabled
816 && !agent_is_fresh
817 && let Some(sanitized_request) = sanitized_request.as_mut()
818 {
819 project_llm_request_to_current_user_turn(
820 sanitized_request,
821 &mut annotation,
822 None,
823 );
824 }
825 let input = sanitized_request
826 .as_ref()
827 .and_then(|request| serde_json::to_value(request).ok());
828 let context = global_context();
829 match context.read() {
830 Ok(state) => state.build_llm_start_event(&queued_handle, input, annotation),
831 Err(_) => event,
832 }
833 })
834 }),
835 event_sanitizers,
836 &subscribers,
837 scope_stack,
838 );
839 Ok(handle)
840}
841
842#[derive(Clone, Copy)]
843struct LlmCallEndBehavior {
844 response_codec_errors_fatal: bool,
845 attach_estimated_cost: bool,
846}
847
848struct LlmEndPayload {
849 data: Option<Json>,
850 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
851 decode_error: Option<FlowError>,
852}
853
854async fn build_llm_end_payload(
855 handle: &LlmHandle,
856 response: Json,
857 fallback_data: Option<Json>,
858 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
859 response_codec: Option<Arc<dyn LlmResponseCodec>>,
860 entries: &[crate::api::registry::Guardrail<crate::api::runtime::LlmSanitizeResponseFn>],
861 behavior: LlmCallEndBehavior,
862) -> LlmEndPayload {
863 let response_was_null_without_fallback = response.is_null() && fallback_data.is_none();
864 let response = if response.is_null() {
865 fallback_data.unwrap_or(response)
866 } else {
867 response
868 };
869 let sanitized_response = NemoRelayContextState::llm_sanitize_response_snapshot_chain(
870 response.clone(),
871 LlmSanitizeResponseContext::for_response_codec(response_codec.clone()),
872 entries,
873 )
874 .await;
875 let response_changed = sanitized_response
876 .as_ref()
877 .is_some_and(|sanitized_response| sanitized_response != &response);
878 let data = match sanitized_response {
879 Some(response) if response_was_null_without_fallback && response.is_null() => None,
880 response => response,
881 };
882 let annotation_omitted = data.as_ref().is_none_or(Json::is_null);
883 let (mut annotated_response, decode_error) = if annotation_omitted {
884 (None, None)
885 } else {
886 resolve_llm_end_annotation(
887 (!response_changed).then_some(annotated_response).flatten(),
888 response_codec,
889 data.as_ref(),
890 &behavior,
891 &handle.name,
892 )
893 };
894 let pricing = crate::codec::response::active_pricing_resolver();
895 let summary = finalize_optimization_summary(
896 &handle.optimization_recorder,
897 annotated_response.as_mut(),
898 handle.model_name.as_deref(),
899 &pricing,
900 );
901 if !annotation_omitted
902 && annotated_response.is_none()
903 && let Some(summary) = summary
904 {
905 annotated_response = Some(AnnotatedLlmResponse {
906 optimization_summary: Some(summary),
907 ..AnnotatedLlmResponse::default()
908 });
909 }
910 LlmEndPayload {
911 data,
912 annotated_response: annotated_response.map(Arc::new),
913 decode_error,
914 }
915}
916
917pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> {
953 ensure_runtime_owner()?;
954 let scope_stack = params.handle.captured_scope_stack().clone();
955 let (entries, subscribers) = {
956 let scope_guard = scope_stack
957 .read()
958 .map_err(|error| FlowError::Internal(error.to_string()))?;
959 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
960 ®istries.llm_sanitize_response_guardrails
961 });
962 let subscribers =
963 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
964 let context = global_context();
965 let state = context
966 .read()
967 .map_err(|error| FlowError::Internal(error.to_string()))?;
968 (
969 state.llm_sanitize_response_entries(&scope_locals),
970 subscribers,
971 )
972 };
973 let response = params.response;
974 let fallback_data = params.data;
975 let handle = params.handle.clone();
976 let metadata = params.metadata;
977 let timestamp = params.timestamp;
978 let annotated_response = params.annotated_response;
979 let response_codec = params.response_codec;
980 handle.optimization_recorder.close_for_finalization(None);
981 enqueue_optimization_marks(&handle, &subscribers);
982 let event = {
983 let context = global_context();
984 let state = context
985 .read()
986 .map_err(|error| FlowError::Internal(error.to_string()))?;
987 state.build_llm_end_event(
988 EndLlmHandleParams::builder()
989 .handle(&handle)
990 .data(Json::Null)
991 .metadata_opt(metadata.clone())
992 .annotated_response_opt(annotated_response.clone())
993 .timestamp_opt(timestamp)
994 .build(),
995 )
996 };
997 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
998 dispatch_transformed_event(
999 event,
1000 Box::new(move |event| {
1001 Box::pin(async move {
1002 let payload = build_llm_end_payload(
1003 &handle,
1004 response,
1005 fallback_data,
1006 annotated_response,
1007 response_codec,
1008 &entries,
1009 LlmCallEndBehavior {
1010 response_codec_errors_fatal: false,
1011 attach_estimated_cost: false,
1012 },
1013 )
1014 .await;
1015 if let Some(error) = payload.decode_error {
1016 log::error!(
1017 target: "nemo_relay.runtime",
1018 event = "manual_llm_response_codec_failed";
1019 "Manual LLM response annotation failed during queued publication: {error}"
1020 );
1021 }
1022 let context = global_context();
1023 let Ok(state) = context.read() else {
1024 return event;
1025 };
1026 let end_metadata = metadata_with_otel_status(metadata, "OK", None);
1027 state.build_llm_end_event(
1028 EndLlmHandleParams::builder()
1029 .handle(&handle)
1030 .data_opt(payload.data)
1031 .metadata_opt(end_metadata)
1032 .annotated_response_opt(payload.annotated_response)
1033 .timestamp_opt(timestamp)
1034 .build(),
1035 )
1036 })
1037 }),
1038 event_sanitizers,
1039 &subscribers,
1040 scope_stack,
1041 );
1042 Ok(())
1043}
1044
1045async fn llm_call_end_with_behavior(
1046 params: LlmCallEndParams<'_>,
1047 behavior: LlmCallEndBehavior,
1048 lifecycle_subscribers: Option<&[EventSubscriberFn]>,
1049) -> Result<()> {
1050 let LlmCallEndParams {
1051 handle,
1052 response,
1053 data,
1054 metadata,
1055 annotated_response,
1056 response_codec,
1057 timestamp,
1058 } = params;
1059 ensure_runtime_owner()?;
1060 let (entries, subscribers) = {
1061 let scope_stack = handle.captured_scope_stack();
1062 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1063 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1064 ®istries.llm_sanitize_response_guardrails
1065 });
1066 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1067 let subscribers = match lifecycle_subscribers {
1068 Some(subscribers) => subscribers.to_vec(),
1069 None => snapshot_event_subscribers(scope_subscribers)?,
1070 };
1071 let context = global_context();
1072 let state = context
1073 .read()
1074 .map_err(|error| FlowError::Internal(error.to_string()))?;
1075 let entries = state.llm_sanitize_response_entries(&scope_locals);
1076 (entries, subscribers)
1077 };
1078 handle.optimization_recorder.close_for_finalization(None);
1079 emit_optimization_marks(handle, &subscribers).await;
1080 let payload = build_llm_end_payload(
1081 handle,
1082 response,
1083 data,
1084 annotated_response,
1085 response_codec,
1086 &entries,
1087 behavior,
1088 )
1089 .await;
1090 let event = {
1091 let context = global_context();
1092 let state = context
1093 .read()
1094 .map_err(|error| FlowError::Internal(error.to_string()))?;
1095 let end_metadata = metadata_with_otel_status(metadata, "OK", None);
1096 state.build_llm_end_event(
1097 EndLlmHandleParams::builder()
1098 .handle(handle)
1099 .data_opt(payload.data)
1100 .metadata_opt(end_metadata)
1101 .annotated_response_opt(payload.annotated_response)
1102 .timestamp_opt(timestamp)
1103 .build(),
1104 )
1105 };
1106 queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack());
1107 if let Some(error) = payload.decode_error
1108 && behavior.response_codec_errors_fatal
1109 {
1110 Err(error)
1111 } else {
1112 Ok(())
1113 }
1114}
1115
1116#[cfg(test)]
1117fn sanitize_context_for_request_codec(codec: Option<&dyn LlmCodec>) -> LlmSanitizeRequestContext {
1118 LlmSanitizeRequestContext::with_identity(
1119 codec.map_or(LlmCodecIdentity::None, LlmCodec::codec_identity),
1120 )
1121}
1122
1123#[cfg(test)]
1124pub(crate) fn sanitize_context_for_response_codec(
1125 codec: Option<&dyn LlmResponseCodec>,
1126) -> LlmSanitizeResponseContext {
1127 LlmSanitizeResponseContext::with_identity(
1128 codec.map_or(LlmCodecIdentity::None, LlmResponseCodec::codec_identity),
1129 )
1130}
1131
1132fn resolve_llm_end_annotation(
1133 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
1134 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1135 data: Option<&Json>,
1136 behavior: &LlmCallEndBehavior,
1137 provider_name: &str,
1138) -> (Option<AnnotatedLlmResponse>, Option<FlowError>) {
1139 if let Some(annotated_response) = annotated_response {
1140 return (Some((*annotated_response).clone()), None);
1141 }
1142 let (Some(codec), Some(response)) = (response_codec, data) else {
1143 return (None, None);
1144 };
1145 match codec.decode_response(response) {
1146 Ok(mut decoded) => {
1147 if behavior.attach_estimated_cost {
1148 attach_estimated_cost_for_provider(&mut decoded, Some(provider_name));
1149 }
1150 (Some(decoded), None)
1151 }
1152 Err(error) => (None, Some(error)),
1153 }
1154}
1155
1156async fn emit_llm_end_without_output(
1157 handle: &LlmHandle,
1158 metadata: Option<Json>,
1159 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1160 lifecycle_subscribers: Option<&[EventSubscriberFn]>,
1161) -> Result<()> {
1162 ensure_runtime_owner()?;
1163 let (entries, subscribers) = {
1164 let scope_stack = handle.captured_scope_stack();
1165 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1166 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1167 ®istries.llm_sanitize_response_guardrails
1168 });
1169 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1170 let subscribers = match lifecycle_subscribers {
1171 Some(subscribers) => subscribers.to_vec(),
1172 None => snapshot_event_subscribers(scope_subscribers)?,
1173 };
1174 let context = global_context();
1175 let state = context
1176 .read()
1177 .map_err(|error| FlowError::Internal(error.to_string()))?;
1178 let entries = state.llm_sanitize_response_entries(&scope_locals);
1179 (entries, subscribers)
1180 };
1181 let had_fallback_data = handle.data.is_some();
1182 let data = if let Some(data) = handle.data.clone() {
1183 NemoRelayContextState::llm_sanitize_response_snapshot_chain(
1184 data,
1185 LlmSanitizeResponseContext::for_response_codec(response_codec),
1186 &entries,
1187 )
1188 .await
1189 } else {
1190 None
1191 };
1192 let annotation_omitted =
1193 (had_fallback_data && data.is_none()) || data.as_ref().is_some_and(Json::is_null);
1194 handle.optimization_recorder.close_for_finalization(None);
1195 emit_optimization_marks(handle, &subscribers).await;
1196 let pricing = crate::codec::response::active_pricing_resolver();
1197 let annotated_response = (!annotation_omitted)
1198 .then(|| {
1199 finalize_optimization_summary(
1200 &handle.optimization_recorder,
1201 None,
1202 handle.model_name.as_deref(),
1203 &pricing,
1204 )
1205 })
1206 .flatten()
1207 .map(|summary| {
1208 Arc::new(AnnotatedLlmResponse {
1209 optimization_summary: Some(summary),
1210 ..AnnotatedLlmResponse::default()
1211 })
1212 });
1213 let event = {
1214 let context = global_context();
1215 let state = context
1216 .read()
1217 .map_err(|error| FlowError::Internal(error.to_string()))?;
1218 state.end_llm_handle(handle, data, metadata, annotated_response)
1219 };
1220 queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack());
1221 Ok(())
1222}
1223
1224struct ManagedLlmCompletion {
1225 handle: Option<LlmHandle>,
1226 metadata: Option<Json>,
1227 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1228 subscribers: Vec<EventSubscriberFn>,
1229 pending_publication: Option<PendingPublication>,
1230}
1231
1232impl ManagedLlmCompletion {
1233 fn new(
1234 handle: &LlmHandle,
1235 metadata: Option<Json>,
1236 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1237 subscribers: &[EventSubscriberFn],
1238 ) -> Self {
1239 Self {
1240 handle: Some(handle.clone()),
1241 metadata,
1242 response_codec,
1243 subscribers: subscribers.to_vec(),
1244 pending_publication: (!subscribers.is_empty())
1245 .then(register_pending_publication)
1246 .flatten(),
1247 }
1248 }
1249
1250 fn disarm(&mut self) {
1251 self.handle = None;
1252 drop(self.pending_publication.take());
1253 }
1254}
1255
1256impl Drop for ManagedLlmCompletion {
1257 fn drop(&mut self) {
1258 let pending_publication = self.pending_publication.take();
1259 let Some(handle) = self.handle.take() else {
1260 return;
1261 };
1262 let metadata = metadata_with_otel_status(
1263 self.metadata.take(),
1264 "ERROR",
1265 Some("LLM execution cancelled".into()),
1266 );
1267 let scope_stack = handle.captured_scope_stack().clone();
1268 let entries = match scope_stack.read() {
1269 Ok(scope_guard) => {
1270 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1271 ®istries.llm_sanitize_response_guardrails
1272 });
1273 global_context()
1274 .read()
1275 .map(|state| state.llm_sanitize_response_entries(&scope_locals))
1276 .unwrap_or_default()
1277 }
1278 Err(_) => Vec::new(),
1279 };
1280 handle
1281 .optimization_recorder
1282 .close_for_finalization(Some("execution_cancelled"));
1283 enqueue_optimization_marks(&handle, &self.subscribers);
1284 let event = global_context()
1285 .read()
1286 .ok()
1287 .map(|state| state.end_llm_handle(&handle, None, metadata.clone(), None));
1288 let Some(event) = event else {
1289 return;
1290 };
1291 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
1292 let response_codec = self.response_codec.take();
1293 let subscribers = std::mem::take(&mut self.subscribers);
1294 let fallback_data = handle.data.clone();
1295 dispatch_transformed_event(
1296 event,
1297 Box::new(move |event| {
1298 Box::pin(async move {
1299 let Some(data) = fallback_data else {
1300 return event;
1301 };
1302 let data = NemoRelayContextState::llm_sanitize_response_snapshot_chain(
1303 data,
1304 LlmSanitizeResponseContext::for_response_codec(response_codec),
1305 &entries,
1306 )
1307 .await;
1308 let annotation_omitted = data.as_ref().is_none_or(Json::is_null);
1309 let annotated_response = (!annotation_omitted)
1310 .then(|| {
1311 let pricing = crate::codec::response::active_pricing_resolver();
1312 finalize_optimization_summary(
1313 &handle.optimization_recorder,
1314 None,
1315 handle.model_name.as_deref(),
1316 &pricing,
1317 )
1318 })
1319 .flatten()
1320 .map(|summary| {
1321 Arc::new(AnnotatedLlmResponse {
1322 optimization_summary: Some(summary),
1323 ..AnnotatedLlmResponse::default()
1324 })
1325 });
1326 global_context()
1327 .read()
1328 .map(|state| {
1329 state.end_llm_handle(&handle, data, metadata, annotated_response)
1330 })
1331 .unwrap_or(event)
1332 })
1333 }),
1334 event_sanitizers,
1335 &subscribers,
1336 scope_stack,
1337 );
1338 drop(pending_publication);
1339 }
1340}
1341
1342pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result<Json> {
1383 let LlmCallExecuteParams {
1384 name,
1385 request,
1386 func,
1387 parent,
1388 attributes,
1389 data,
1390 metadata,
1391 model_name,
1392 codec,
1393 response_codec,
1394 } = params;
1395 ensure_runtime_owner()?;
1396 {
1397 let (entries, subscribers, parent_uuid, guardrail_metadata) = {
1398 let scope_stack = current_scope_stack();
1399 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1400 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1401 ®istries.llm_conditional_execution_guardrails
1402 });
1403 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1404 let context = global_context();
1405 let state = context
1406 .read()
1407 .map_err(|error| FlowError::Internal(error.to_string()))?;
1408 let entries = state.llm_conditional_execution_entries(&scope_locals);
1409 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1410 (
1411 entries,
1412 subscribers,
1413 resolve_parent_uuid(parent.as_ref()),
1414 metadata.clone(),
1415 )
1416 };
1417 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1418 &request,
1419 &entries,
1420 &subscribers,
1421 parent_uuid,
1422 guardrail_metadata,
1423 )
1424 .await?
1425 {
1426 let mut rejection_data = json!({});
1427 if let Some(object) = rejection_data.as_object_mut() {
1428 object.insert("rejected".into(), json!(true));
1429 object.insert("rejection_reason".into(), json!(&error));
1430 }
1431 let _ = event(
1432 EmitMarkEventParams::builder()
1433 .name(&name)
1434 .parent_opt(parent.as_ref())
1435 .data(rejection_data)
1436 .metadata_opt(metadata.clone())
1437 .build(),
1438 );
1439 return Err(FlowError::GuardrailRejected(error));
1440 }
1441 }
1442
1443 let request_codec = codec.clone();
1444 let optimization_recorder = LlmOptimizationRecorder::default();
1445 let (intercepted_request, annotated_request, pending_marks, optimization_contributions) =
1446 scope_llm_optimization_recorder(optimization_recorder.clone(), async {
1447 run_request_intercepts_with_codec_and_recorder(
1448 &name,
1449 request,
1450 codec,
1451 &optimization_recorder,
1452 )
1453 .await
1454 })
1455 .await?;
1456
1457 let mut handle = create_llm_handle(
1458 CreateLlmHandleParams::builder()
1459 .name(name.as_str())
1460 .parent_uuid_opt(resolve_parent_uuid(parent.as_ref()))
1461 .attributes(attributes)
1462 .data_opt(data.clone())
1463 .metadata_opt(metadata.clone())
1464 .model_name_opt(model_name)
1465 .build(),
1466 )?;
1467 handle.optimization_recorder = optimization_recorder;
1468 let lifecycle_subscribers = {
1469 let scope_stack = handle.captured_scope_stack();
1470 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1471 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
1472 };
1473 emit_llm_start_with_subscribers(
1474 &handle,
1475 &intercepted_request,
1476 annotated_request.clone(),
1477 request_codec.clone(),
1478 &lifecycle_subscribers,
1479 )
1480 .await?;
1481 emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?;
1482 handle
1483 .optimization_recorder
1484 .record_all(optimization_contributions);
1485 emit_optimization_marks(&handle, &lifecycle_subscribers).await;
1486
1487 let mut completion = ManagedLlmCompletion::new(
1488 &handle,
1489 metadata.clone(),
1490 response_codec.clone(),
1491 &lifecycle_subscribers,
1492 );
1493 let execution_name = name.clone();
1494 let event_uuid = handle.uuid;
1495 let execution = with_active_event_uuid(
1496 event_uuid,
1497 scope_llm_optimization_recorder(handle.optimization_recorder.clone(), async move {
1498 let execution = {
1499 let scope_stack = current_scope_stack();
1500 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1501 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1502 ®istries.llm_execution_intercepts
1503 });
1504 let context = global_context();
1505 let state = context
1506 .read()
1507 .map_err(|error| FlowError::Internal(error.to_string()))?;
1508 state.llm_build_execution_chain(&execution_name, func, &scope_locals)
1509 };
1510 execution(intercepted_request).await
1511 }),
1512 )
1513 .await;
1514
1515 match execution {
1516 Ok(response) => {
1517 llm_call_end_with_behavior(
1518 LlmCallEndParams::builder()
1519 .handle(&handle)
1520 .response(response.clone())
1521 .data_opt(data)
1522 .metadata_opt(metadata)
1523 .response_codec_opt(response_codec)
1524 .build(),
1525 LlmCallEndBehavior {
1526 response_codec_errors_fatal: false,
1527 attach_estimated_cost: true,
1528 },
1529 Some(&lifecycle_subscribers),
1530 )
1531 .await?;
1532 completion.disarm();
1533 Ok(response)
1534 }
1535 Err(error) => {
1536 let end_metadata = metadata_with_otel_error(metadata, &error);
1537 let _ = emit_llm_end_without_output(
1538 &handle,
1539 end_metadata,
1540 response_codec,
1541 Some(&lifecycle_subscribers),
1542 )
1543 .await;
1544 completion.disarm();
1545 Err(error)
1546 }
1547 }
1548}
1549
1550pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Result<LlmJsonStream> {
1590 let LlmStreamCallExecuteParams {
1591 name,
1592 request,
1593 func,
1594 collector,
1595 finalizer,
1596 parent,
1597 attributes,
1598 data,
1599 metadata,
1600 model_name,
1601 codec,
1602 response_codec,
1603 } = params;
1604 ensure_runtime_owner()?;
1605 {
1606 let (entries, subscribers, parent_uuid, guardrail_metadata) = {
1607 let scope_stack = current_scope_stack();
1608 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1609 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1610 ®istries.llm_conditional_execution_guardrails
1611 });
1612 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1613 let context = global_context();
1614 let state = context
1615 .read()
1616 .map_err(|error| FlowError::Internal(error.to_string()))?;
1617 let entries = state.llm_conditional_execution_entries(&scope_locals);
1618 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1619 (
1620 entries,
1621 subscribers,
1622 resolve_parent_uuid(parent.as_ref()),
1623 metadata.clone(),
1624 )
1625 };
1626 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1627 &request,
1628 &entries,
1629 &subscribers,
1630 parent_uuid,
1631 guardrail_metadata,
1632 )
1633 .await?
1634 {
1635 let mut rejection_data = json!({});
1636 if let Some(object) = rejection_data.as_object_mut() {
1637 object.insert("rejected".into(), json!(true));
1638 object.insert("rejection_reason".into(), json!(&error));
1639 }
1640 let _ = event(
1641 EmitMarkEventParams::builder()
1642 .name(&name)
1643 .parent_opt(parent.as_ref())
1644 .data(rejection_data)
1645 .metadata_opt(metadata.clone())
1646 .build(),
1647 );
1648 return Err(FlowError::GuardrailRejected(error));
1649 }
1650 }
1651
1652 let request_codec = codec.clone();
1653 let optimization_recorder = LlmOptimizationRecorder::default();
1654 let (intercepted_request, annotated_request, pending_marks, optimization_contributions) =
1655 scope_llm_optimization_recorder(optimization_recorder.clone(), async {
1656 run_request_intercepts_with_codec_and_recorder(
1657 &name,
1658 request,
1659 codec,
1660 &optimization_recorder,
1661 )
1662 .await
1663 })
1664 .await?;
1665
1666 let mut handle = create_llm_handle(
1667 CreateLlmHandleParams::builder()
1668 .name(name.as_str())
1669 .parent_uuid_opt(resolve_parent_uuid(parent.as_ref()))
1670 .attributes(attributes)
1671 .data_opt(data.clone())
1672 .metadata_opt(metadata.clone())
1673 .model_name_opt(model_name)
1674 .build(),
1675 )?;
1676 handle.optimization_recorder = optimization_recorder;
1677 let lifecycle_subscribers = {
1678 let scope_stack = handle.captured_scope_stack();
1679 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1680 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
1681 };
1682 emit_llm_start_with_subscribers(
1683 &handle,
1684 &intercepted_request,
1685 annotated_request,
1686 request_codec.clone(),
1687 &lifecycle_subscribers,
1688 )
1689 .await?;
1690 emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?;
1691 handle
1692 .optimization_recorder
1693 .record_all(optimization_contributions);
1694 emit_optimization_marks(&handle, &lifecycle_subscribers).await;
1695
1696 let mut completion = ManagedLlmCompletion::new(
1697 &handle,
1698 metadata.clone(),
1699 response_codec.clone(),
1700 &lifecycle_subscribers,
1701 );
1702 let execution_name = name.clone();
1703 let event_uuid = handle.uuid;
1704 let execution = with_active_event_uuid(
1705 event_uuid,
1706 scope_llm_optimization_recorder(handle.optimization_recorder.clone(), async move {
1707 let execution = {
1708 let scope_stack = current_scope_stack();
1709 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1710 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1711 ®istries.llm_stream_execution_intercepts
1712 });
1713 let context = global_context();
1714 let state = context
1715 .read()
1716 .map_err(|error| FlowError::Internal(error.to_string()))?;
1717 state.llm_stream_build_execution_chain(&execution_name, func, &scope_locals)
1718 };
1719 let execution_context = MiddlewareContinuationContext::capture();
1720 execution(intercepted_request)
1721 .await
1722 .map(|stream| contextualize_stream(stream, execution_context))
1723 }),
1724 )
1725 .await;
1726
1727 match execution {
1728 Ok(raw_stream) => {
1729 let wrapper = LlmStreamWrapper::new_managed(
1730 raw_stream,
1731 handle,
1732 collector,
1733 finalizer,
1734 metadata,
1735 response_codec,
1736 lifecycle_subscribers,
1737 );
1738 completion.disarm();
1739 Ok(LlmJsonStream::from_closeable(wrapper))
1740 }
1741 Err(error) => {
1742 let end_metadata = metadata_with_otel_error(metadata, &error);
1743 let _ = emit_llm_end_without_output(
1744 &handle,
1745 end_metadata,
1746 response_codec,
1747 Some(&lifecycle_subscribers),
1748 )
1749 .await;
1750 completion.disarm();
1751 Err(error)
1752 }
1753 }
1754}
1755
1756pub async fn llm_request_intercepts(
1780 name: &str,
1781 request: LlmRequest,
1782) -> Result<LlmRequestInterceptOutcome> {
1783 ensure_runtime_owner()?;
1784 let entries = {
1785 let scope_stack = current_scope_stack();
1786 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1787 let scope_locals = scope_guard
1788 .collect_scope_local_registries(|registries| ®istries.llm_request_intercepts);
1789 let context = global_context();
1790 let state = context
1791 .read()
1792 .map_err(|error| FlowError::Internal(error.to_string()))?;
1793 state.llm_request_intercept_entries(&scope_locals)
1794 };
1795 let mut outcome = NemoRelayContextState::llm_request_intercepts_snapshot_chain(
1796 name, request, None, &entries, false,
1797 )
1798 .await?;
1799 inject_dynamo_session_ids(&mut outcome.request);
1800 Ok(outcome)
1801}
1802
1803pub async fn llm_conditional_execution(request: &LlmRequest) -> Result<()> {
1824 ensure_runtime_owner()?;
1825 let (entries, subscribers, parent_uuid) = {
1826 let scope_stack = current_scope_stack();
1827 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1828 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1829 ®istries.llm_conditional_execution_guardrails
1830 });
1831 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1832 let context = global_context();
1833 let state = context
1834 .read()
1835 .map_err(|error| FlowError::Internal(error.to_string()))?;
1836 let entries = state.llm_conditional_execution_entries(&scope_locals);
1837 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1838 (entries, subscribers, resolve_parent_uuid(None))
1839 };
1840 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1841 request,
1842 &entries,
1843 &subscribers,
1844 parent_uuid,
1845 None,
1846 )
1847 .await?
1848 {
1849 return Err(FlowError::GuardrailRejected(error));
1850 }
1851 Ok(())
1852}
1853
1854#[cfg(test)]
1855#[path = "../../tests/unit/llm_api_tests.rs"]
1856mod tests;