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 = {
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 state.llm_sanitize_request_entries(&scope_locals)
437 };
438 let observable_request = remove_observability_credential_headers(request.clone());
439 let mut sanitized_request = NemoRelayContextState::llm_sanitize_request_snapshot_chain(
440 observable_request.clone(),
441 LlmSanitizeRequestContext::for_request_codec(request_codec.clone()),
442 &entries,
443 )
444 .await;
445 let request_changed = sanitized_request
446 .as_ref()
447 .is_some_and(|sanitized_request| sanitized_request != &observable_request);
448 let mut annotated_request = match (sanitized_request.as_ref(), request_codec.as_deref()) {
449 (Some(sanitized_request), Some(codec)) if request_changed => {
450 codec.decode(sanitized_request).ok().map(Arc::new)
451 }
452 (Some(_), _) if !request_changed => annotated_request,
453 (None, _) => None,
454 (Some(_), _) => None,
455 };
456 let scope_stack = handle.captured_scope_stack();
457 let agent_is_fresh = {
458 let mut scope_guard = scope_stack.write().expect("scope stack lock poisoned");
459 scope_guard.take_agent_freshness(handle.parent_uuid)
460 };
461 if !agent_is_fresh && let Some(sanitized_request) = sanitized_request.as_mut() {
462 project_llm_request_to_current_user_turn(
463 sanitized_request,
464 &mut annotated_request,
465 request_codec.as_deref(),
466 );
467 }
468 let input = sanitized_request
469 .as_ref()
470 .and_then(|sanitized_request| serde_json::to_value(sanitized_request).ok());
471 let event = {
472 let context = global_context();
473 let state = context
474 .read()
475 .map_err(|error| FlowError::Internal(error.to_string()))?;
476 state.build_llm_start_event(handle, input, annotated_request)
477 };
478 queue_sanitized_event_with_scope_stack(event, subscribers, scope_stack);
479 Ok(())
480}
481
482fn remove_observability_credential_headers(mut request: LlmRequest) -> LlmRequest {
483 request.headers.retain(|name, _| {
484 !OBSERVABILITY_CREDENTIAL_HEADERS
485 .iter()
486 .any(|credential_header| name.eq_ignore_ascii_case(credential_header))
487 });
488 request
489}
490
491#[cfg(test)]
495fn emit_llm_start(
496 handle: &LlmHandle,
497 request: &LlmRequest,
498 annotated_request: Option<Arc<AnnotatedLlmRequest>>,
499 request_codec: Option<Arc<dyn LlmCodec>>,
500) -> Result<()> {
501 let subscribers = {
502 let scope_stack = handle.captured_scope_stack();
503 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
504 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
505 };
506 crate::api::runtime::subscriber_dispatcher::block_on_sanitizer_future(
507 emit_llm_start_with_subscribers(
508 handle,
509 request,
510 annotated_request,
511 request_codec,
512 &subscribers,
513 ),
514 )
515 .map_err(FlowError::Internal)?
516}
517
518async fn emit_pending_request_marks(
519 handle: &LlmHandle,
520 marks: Vec<PendingMarkSpec>,
521 subscribers: &[EventSubscriberFn],
522) -> Result<()> {
523 if marks.is_empty() {
524 return Ok(());
525 }
526 ensure_runtime_owner()?;
527 let timestamp = handle.started_at + TimeDelta::microseconds(1);
528 for mark in marks {
529 let event = Event::Mark(MarkEvent::new(
530 BaseEvent::builder()
531 .name(mark.name)
532 .parent_uuid(handle.uuid)
533 .timestamp(timestamp)
534 .data_opt(mark.data)
535 .metadata_opt(mark.metadata)
536 .build(),
537 mark.category,
538 mark.category_profile,
539 ));
540 queue_sanitized_event_with_scope_stack(event, subscribers, handle.captured_scope_stack());
541 }
542 Ok(())
543}
544
545pub(crate) async fn emit_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) {
546 emit_optimization_marks_with_async(
547 handle,
548 subscribers,
549 |event| async { Some(event) },
550 |event, subscribers| {
551 queue_sanitized_event_with_scope_stack(
552 event.clone(),
553 subscribers,
554 handle.captured_scope_stack(),
555 )
556 },
557 )
558 .await;
559}
560
561pub(crate) async fn emit_reserved_optimization_marks(
562 handle: &LlmHandle,
563 subscribers: &[EventSubscriberFn],
564) {
565 emit_optimization_marks_with_async(
566 handle,
567 subscribers,
568 |event| async { Some(event) },
569 |event, subscribers| {
570 let sanitizers =
571 snapshot_event_sanitizers(event, handle.captured_scope_stack()).unwrap_or_default();
572 dispatch_reserved_sanitized_event(
573 event.clone(),
574 sanitizers,
575 subscribers,
576 handle.captured_scope_stack().clone(),
577 )
578 },
579 )
580 .await;
581}
582
583fn enqueue_optimization_marks(handle: &LlmHandle, subscribers: &[EventSubscriberFn]) {
589 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
590 if contributions.is_empty() || ensure_runtime_owner().is_err() {
591 return;
592 }
593 let scope_stack = handle.captured_scope_stack().clone();
594 for (contribution, recorded_at) in contributions {
595 let event = optimization_mark_event(handle, &contribution, recorded_at);
596 let sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
597 if dispatch_sanitized_event(event, sanitizers, subscribers, scope_stack.clone()) {
598 handle.optimization_recorder.mark_emitted(1);
599 } else {
600 break;
601 }
602 }
603}
604
605async fn emit_optimization_marks_with_async<F, Fut>(
606 handle: &LlmHandle,
607 subscribers: &[EventSubscriberFn],
608 mut sanitize: F,
609 mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool,
610) where
611 F: FnMut(Event) -> Fut,
612 Fut: Future<Output = Option<Event>>,
613{
614 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
615 if contributions.is_empty() {
616 return;
617 }
618 if ensure_runtime_owner().is_err() {
619 log::warn!(
620 target: "nemo_relay.observability",
621 event = "optimization_marks_skipped",
622 reason = "runtime_owner_unavailable",
623 contribution_count = contributions.len();
624 "LLM optimization marks were skipped"
625 );
626 return;
627 }
628 for (contribution, recorded_at) in contributions {
629 let event = optimization_mark_event(handle, &contribution, recorded_at);
630 let Some(event) = sanitize(event).await else {
631 break;
635 };
636 if enqueue(&event, subscribers) {
637 handle.optimization_recorder.mark_emitted(1);
638 } else {
639 break;
643 }
644 }
645}
646
647fn optimization_mark_event(
648 handle: &LlmHandle,
649 contribution: &crate::codec::optimization::LlmOptimizationContribution,
650 recorded_at: DateTime<Utc>,
651) -> Event {
652 let offset = contribution.sequence.unwrap_or(0).saturating_add(2);
653 let offset = i64::try_from(offset).unwrap_or(i64::MAX);
654 let request_ordered_timestamp = handle.started_at + TimeDelta::microseconds(offset);
655 Event::Mark(MarkEvent::new(
656 BaseEvent::builder()
657 .name("nemo_relay.llm.optimization")
658 .parent_uuid(handle.uuid)
659 .timestamp(recorded_at.max(request_ordered_timestamp))
660 .data(serde_json::to_value(contribution).unwrap_or(Json::Null))
661 .data_schema(DataSchema {
662 name: "nemo.relay.llm_optimization_contribution".to_string(),
663 version: "1".to_string(),
664 })
665 .build(),
666 Some(EventCategory::custom()),
667 Some(
668 CategoryProfile::builder()
669 .subtype("nemo_relay.llm.optimization")
670 .build(),
671 ),
672 ))
673}
674
675#[cfg(test)]
679fn emit_optimization_marks_with<F>(
680 handle: &LlmHandle,
681 subscribers: &[EventSubscriberFn],
682 mut sanitize: F,
683 mut enqueue: impl FnMut(&Event, &[EventSubscriberFn]) -> bool,
684) where
685 F: FnMut(Event) -> Option<Event>,
686{
687 let contributions = handle.optimization_recorder.unemitted_with_timestamps();
688 if contributions.is_empty() || ensure_runtime_owner().is_err() {
689 return;
690 }
691 for (contribution, recorded_at) in contributions {
692 let event = optimization_mark_event(handle, &contribution, recorded_at);
693 let Some(event) = sanitize(event) else {
694 break;
695 };
696 if enqueue(&event, subscribers) {
697 handle.optimization_recorder.mark_emitted(1);
698 } else {
699 break;
700 }
701 }
702}
703
704pub fn llm_call(params: LlmCallParams<'_>) -> Result<LlmHandle> {
746 let handle_params = CreateLlmHandleParams::builder()
747 .name(params.name)
748 .parent_uuid_opt(resolve_parent_uuid(params.parent))
749 .attributes(params.attributes)
750 .data_opt(params.data)
751 .metadata_opt(params.metadata)
752 .model_name_opt(params.model_name)
753 .timestamp_opt(params.timestamp)
754 .build();
755 let handle = create_llm_handle(handle_params)?;
756 let scope_stack = handle.captured_scope_stack().clone();
757 let (entries, subscribers, agent_is_fresh) = {
758 let mut scope_guard = scope_stack
759 .write()
760 .map_err(|error| FlowError::Internal(error.to_string()))?;
761 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
762 ®istries.llm_sanitize_request_guardrails
763 });
764 let subscribers =
765 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
766 let context = global_context();
767 let state = context
768 .read()
769 .map_err(|error| FlowError::Internal(error.to_string()))?;
770 let entries = state.llm_sanitize_request_entries(&scope_locals);
771 drop(state);
772 let agent_is_fresh = scope_guard.take_agent_freshness(handle.parent_uuid);
773 (entries, subscribers, agent_is_fresh)
774 };
775 let request = remove_observability_credential_headers(params.request.clone());
778 let annotated_request = params.annotated_request;
779 let event = {
780 let context = global_context();
781 let state = context
782 .read()
783 .map_err(|error| FlowError::Internal(error.to_string()))?;
784 state.build_llm_start_event(&handle, None, None)
785 };
786 let queued_handle = handle.clone();
787 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
788 dispatch_transformed_event(
789 event,
790 Box::new(move |event| {
791 Box::pin(async move {
792 let mut sanitized_request =
793 NemoRelayContextState::llm_sanitize_request_snapshot_chain(
794 request.clone(),
795 LlmSanitizeRequestContext::default(),
796 &entries,
797 )
798 .await;
799 let request_changed = sanitized_request
800 .as_ref()
801 .is_some_and(|sanitized| sanitized != &request);
802 let mut annotation = if sanitized_request.is_none() || request_changed {
803 None
804 } else {
805 annotated_request
806 };
807 if !agent_is_fresh && let Some(sanitized_request) = sanitized_request.as_mut() {
808 project_llm_request_to_current_user_turn(
809 sanitized_request,
810 &mut annotation,
811 None,
812 );
813 }
814 let input = sanitized_request
815 .as_ref()
816 .and_then(|request| serde_json::to_value(request).ok());
817 let context = global_context();
818 match context.read() {
819 Ok(state) => state.build_llm_start_event(&queued_handle, input, annotation),
820 Err(_) => event,
821 }
822 })
823 }),
824 event_sanitizers,
825 &subscribers,
826 scope_stack,
827 );
828 Ok(handle)
829}
830
831#[derive(Clone, Copy)]
832struct LlmCallEndBehavior {
833 response_codec_errors_fatal: bool,
834 attach_estimated_cost: bool,
835}
836
837struct LlmEndPayload {
838 data: Option<Json>,
839 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
840 decode_error: Option<FlowError>,
841}
842
843async fn build_llm_end_payload(
844 handle: &LlmHandle,
845 response: Json,
846 fallback_data: Option<Json>,
847 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
848 response_codec: Option<Arc<dyn LlmResponseCodec>>,
849 entries: &[crate::api::registry::Guardrail<crate::api::runtime::LlmSanitizeResponseFn>],
850 behavior: LlmCallEndBehavior,
851) -> LlmEndPayload {
852 let response_was_null_without_fallback = response.is_null() && fallback_data.is_none();
853 let response = if response.is_null() {
854 fallback_data.unwrap_or(response)
855 } else {
856 response
857 };
858 let sanitized_response = NemoRelayContextState::llm_sanitize_response_snapshot_chain(
859 response.clone(),
860 LlmSanitizeResponseContext::for_response_codec(response_codec.clone()),
861 entries,
862 )
863 .await;
864 let response_changed = sanitized_response
865 .as_ref()
866 .is_some_and(|sanitized_response| sanitized_response != &response);
867 let data = match sanitized_response {
868 Some(response) if response_was_null_without_fallback && response.is_null() => None,
869 response => response,
870 };
871 let annotation_omitted = data.as_ref().is_none_or(Json::is_null);
872 let (mut annotated_response, decode_error) = if annotation_omitted {
873 (None, None)
874 } else {
875 resolve_llm_end_annotation(
876 (!response_changed).then_some(annotated_response).flatten(),
877 response_codec,
878 data.as_ref(),
879 &behavior,
880 &handle.name,
881 )
882 };
883 let pricing = crate::codec::response::active_pricing_resolver();
884 let summary = finalize_optimization_summary(
885 &handle.optimization_recorder,
886 annotated_response.as_mut(),
887 handle.model_name.as_deref(),
888 &pricing,
889 );
890 if !annotation_omitted
891 && annotated_response.is_none()
892 && let Some(summary) = summary
893 {
894 annotated_response = Some(AnnotatedLlmResponse {
895 optimization_summary: Some(summary),
896 ..AnnotatedLlmResponse::default()
897 });
898 }
899 LlmEndPayload {
900 data,
901 annotated_response: annotated_response.map(Arc::new),
902 decode_error,
903 }
904}
905
906pub fn llm_call_end(params: LlmCallEndParams<'_>) -> Result<()> {
942 ensure_runtime_owner()?;
943 let scope_stack = params.handle.captured_scope_stack().clone();
944 let (entries, subscribers) = {
945 let scope_guard = scope_stack
946 .read()
947 .map_err(|error| FlowError::Internal(error.to_string()))?;
948 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
949 ®istries.llm_sanitize_response_guardrails
950 });
951 let subscribers =
952 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?;
953 let context = global_context();
954 let state = context
955 .read()
956 .map_err(|error| FlowError::Internal(error.to_string()))?;
957 (
958 state.llm_sanitize_response_entries(&scope_locals),
959 subscribers,
960 )
961 };
962 let response = params.response;
963 let fallback_data = params.data;
964 let handle = params.handle.clone();
965 let metadata = params.metadata;
966 let timestamp = params.timestamp;
967 let annotated_response = params.annotated_response;
968 let response_codec = params.response_codec;
969 handle.optimization_recorder.close_for_finalization(None);
970 enqueue_optimization_marks(&handle, &subscribers);
971 let event = {
972 let context = global_context();
973 let state = context
974 .read()
975 .map_err(|error| FlowError::Internal(error.to_string()))?;
976 state.build_llm_end_event(
977 EndLlmHandleParams::builder()
978 .handle(&handle)
979 .data(Json::Null)
980 .metadata_opt(metadata.clone())
981 .annotated_response_opt(annotated_response.clone())
982 .timestamp_opt(timestamp)
983 .build(),
984 )
985 };
986 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
987 dispatch_transformed_event(
988 event,
989 Box::new(move |event| {
990 Box::pin(async move {
991 let payload = build_llm_end_payload(
992 &handle,
993 response,
994 fallback_data,
995 annotated_response,
996 response_codec,
997 &entries,
998 LlmCallEndBehavior {
999 response_codec_errors_fatal: false,
1000 attach_estimated_cost: false,
1001 },
1002 )
1003 .await;
1004 if let Some(error) = payload.decode_error {
1005 log::error!(
1006 target: "nemo_relay.runtime",
1007 event = "manual_llm_response_codec_failed";
1008 "Manual LLM response annotation failed during queued publication: {error}"
1009 );
1010 }
1011 let context = global_context();
1012 let Ok(state) = context.read() else {
1013 return event;
1014 };
1015 let end_metadata = metadata_with_otel_status(metadata, "OK", None);
1016 state.build_llm_end_event(
1017 EndLlmHandleParams::builder()
1018 .handle(&handle)
1019 .data_opt(payload.data)
1020 .metadata_opt(end_metadata)
1021 .annotated_response_opt(payload.annotated_response)
1022 .timestamp_opt(timestamp)
1023 .build(),
1024 )
1025 })
1026 }),
1027 event_sanitizers,
1028 &subscribers,
1029 scope_stack,
1030 );
1031 Ok(())
1032}
1033
1034async fn llm_call_end_with_behavior(
1035 params: LlmCallEndParams<'_>,
1036 behavior: LlmCallEndBehavior,
1037 lifecycle_subscribers: Option<&[EventSubscriberFn]>,
1038) -> Result<()> {
1039 let LlmCallEndParams {
1040 handle,
1041 response,
1042 data,
1043 metadata,
1044 annotated_response,
1045 response_codec,
1046 timestamp,
1047 } = params;
1048 ensure_runtime_owner()?;
1049 let (entries, subscribers) = {
1050 let scope_stack = handle.captured_scope_stack();
1051 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1052 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1053 ®istries.llm_sanitize_response_guardrails
1054 });
1055 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1056 let subscribers = match lifecycle_subscribers {
1057 Some(subscribers) => subscribers.to_vec(),
1058 None => snapshot_event_subscribers(scope_subscribers)?,
1059 };
1060 let context = global_context();
1061 let state = context
1062 .read()
1063 .map_err(|error| FlowError::Internal(error.to_string()))?;
1064 let entries = state.llm_sanitize_response_entries(&scope_locals);
1065 (entries, subscribers)
1066 };
1067 handle.optimization_recorder.close_for_finalization(None);
1068 emit_optimization_marks(handle, &subscribers).await;
1069 let payload = build_llm_end_payload(
1070 handle,
1071 response,
1072 data,
1073 annotated_response,
1074 response_codec,
1075 &entries,
1076 behavior,
1077 )
1078 .await;
1079 let event = {
1080 let context = global_context();
1081 let state = context
1082 .read()
1083 .map_err(|error| FlowError::Internal(error.to_string()))?;
1084 let end_metadata = metadata_with_otel_status(metadata, "OK", None);
1085 state.build_llm_end_event(
1086 EndLlmHandleParams::builder()
1087 .handle(handle)
1088 .data_opt(payload.data)
1089 .metadata_opt(end_metadata)
1090 .annotated_response_opt(payload.annotated_response)
1091 .timestamp_opt(timestamp)
1092 .build(),
1093 )
1094 };
1095 queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack());
1096 if let Some(error) = payload.decode_error
1097 && behavior.response_codec_errors_fatal
1098 {
1099 Err(error)
1100 } else {
1101 Ok(())
1102 }
1103}
1104
1105#[cfg(test)]
1106fn sanitize_context_for_request_codec(codec: Option<&dyn LlmCodec>) -> LlmSanitizeRequestContext {
1107 LlmSanitizeRequestContext::with_identity(
1108 codec.map_or(LlmCodecIdentity::None, LlmCodec::codec_identity),
1109 )
1110}
1111
1112#[cfg(test)]
1113pub(crate) fn sanitize_context_for_response_codec(
1114 codec: Option<&dyn LlmResponseCodec>,
1115) -> LlmSanitizeResponseContext {
1116 LlmSanitizeResponseContext::with_identity(
1117 codec.map_or(LlmCodecIdentity::None, LlmResponseCodec::codec_identity),
1118 )
1119}
1120
1121fn resolve_llm_end_annotation(
1122 annotated_response: Option<Arc<AnnotatedLlmResponse>>,
1123 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1124 data: Option<&Json>,
1125 behavior: &LlmCallEndBehavior,
1126 provider_name: &str,
1127) -> (Option<AnnotatedLlmResponse>, Option<FlowError>) {
1128 if let Some(annotated_response) = annotated_response {
1129 return (Some((*annotated_response).clone()), None);
1130 }
1131 let (Some(codec), Some(response)) = (response_codec, data) else {
1132 return (None, None);
1133 };
1134 match codec.decode_response(response) {
1135 Ok(mut decoded) => {
1136 if behavior.attach_estimated_cost {
1137 attach_estimated_cost_for_provider(&mut decoded, Some(provider_name));
1138 }
1139 (Some(decoded), None)
1140 }
1141 Err(error) => (None, Some(error)),
1142 }
1143}
1144
1145async fn emit_llm_end_without_output(
1146 handle: &LlmHandle,
1147 metadata: Option<Json>,
1148 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1149 lifecycle_subscribers: Option<&[EventSubscriberFn]>,
1150) -> Result<()> {
1151 ensure_runtime_owner()?;
1152 let (entries, subscribers) = {
1153 let scope_stack = handle.captured_scope_stack();
1154 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1155 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1156 ®istries.llm_sanitize_response_guardrails
1157 });
1158 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1159 let subscribers = match lifecycle_subscribers {
1160 Some(subscribers) => subscribers.to_vec(),
1161 None => snapshot_event_subscribers(scope_subscribers)?,
1162 };
1163 let context = global_context();
1164 let state = context
1165 .read()
1166 .map_err(|error| FlowError::Internal(error.to_string()))?;
1167 let entries = state.llm_sanitize_response_entries(&scope_locals);
1168 (entries, subscribers)
1169 };
1170 let had_fallback_data = handle.data.is_some();
1171 let data = if let Some(data) = handle.data.clone() {
1172 NemoRelayContextState::llm_sanitize_response_snapshot_chain(
1173 data,
1174 LlmSanitizeResponseContext::for_response_codec(response_codec),
1175 &entries,
1176 )
1177 .await
1178 } else {
1179 None
1180 };
1181 let annotation_omitted =
1182 (had_fallback_data && data.is_none()) || data.as_ref().is_some_and(Json::is_null);
1183 handle.optimization_recorder.close_for_finalization(None);
1184 emit_optimization_marks(handle, &subscribers).await;
1185 let pricing = crate::codec::response::active_pricing_resolver();
1186 let annotated_response = (!annotation_omitted)
1187 .then(|| {
1188 finalize_optimization_summary(
1189 &handle.optimization_recorder,
1190 None,
1191 handle.model_name.as_deref(),
1192 &pricing,
1193 )
1194 })
1195 .flatten()
1196 .map(|summary| {
1197 Arc::new(AnnotatedLlmResponse {
1198 optimization_summary: Some(summary),
1199 ..AnnotatedLlmResponse::default()
1200 })
1201 });
1202 let event = {
1203 let context = global_context();
1204 let state = context
1205 .read()
1206 .map_err(|error| FlowError::Internal(error.to_string()))?;
1207 state.end_llm_handle(handle, data, metadata, annotated_response)
1208 };
1209 queue_sanitized_event_with_scope_stack(event, &subscribers, handle.captured_scope_stack());
1210 Ok(())
1211}
1212
1213struct ManagedLlmCompletion {
1214 handle: Option<LlmHandle>,
1215 metadata: Option<Json>,
1216 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1217 subscribers: Vec<EventSubscriberFn>,
1218 pending_publication: Option<PendingPublication>,
1219}
1220
1221impl ManagedLlmCompletion {
1222 fn new(
1223 handle: &LlmHandle,
1224 metadata: Option<Json>,
1225 response_codec: Option<Arc<dyn LlmResponseCodec>>,
1226 subscribers: &[EventSubscriberFn],
1227 ) -> Self {
1228 Self {
1229 handle: Some(handle.clone()),
1230 metadata,
1231 response_codec,
1232 subscribers: subscribers.to_vec(),
1233 pending_publication: (!subscribers.is_empty())
1234 .then(register_pending_publication)
1235 .flatten(),
1236 }
1237 }
1238
1239 fn disarm(&mut self) {
1240 self.handle = None;
1241 drop(self.pending_publication.take());
1242 }
1243}
1244
1245impl Drop for ManagedLlmCompletion {
1246 fn drop(&mut self) {
1247 let pending_publication = self.pending_publication.take();
1248 let Some(handle) = self.handle.take() else {
1249 return;
1250 };
1251 let metadata = metadata_with_otel_status(
1252 self.metadata.take(),
1253 "ERROR",
1254 Some("LLM execution cancelled".into()),
1255 );
1256 let scope_stack = handle.captured_scope_stack().clone();
1257 let entries = match scope_stack.read() {
1258 Ok(scope_guard) => {
1259 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1260 ®istries.llm_sanitize_response_guardrails
1261 });
1262 global_context()
1263 .read()
1264 .map(|state| state.llm_sanitize_response_entries(&scope_locals))
1265 .unwrap_or_default()
1266 }
1267 Err(_) => Vec::new(),
1268 };
1269 handle
1270 .optimization_recorder
1271 .close_for_finalization(Some("execution_cancelled"));
1272 enqueue_optimization_marks(&handle, &self.subscribers);
1273 let event = global_context()
1274 .read()
1275 .ok()
1276 .map(|state| state.end_llm_handle(&handle, None, metadata.clone(), None));
1277 let Some(event) = event else {
1278 return;
1279 };
1280 let event_sanitizers = snapshot_event_sanitizers(&event, &scope_stack).unwrap_or_default();
1281 let response_codec = self.response_codec.take();
1282 let subscribers = std::mem::take(&mut self.subscribers);
1283 let fallback_data = handle.data.clone();
1284 dispatch_transformed_event(
1285 event,
1286 Box::new(move |event| {
1287 Box::pin(async move {
1288 let Some(data) = fallback_data else {
1289 return event;
1290 };
1291 let data = NemoRelayContextState::llm_sanitize_response_snapshot_chain(
1292 data,
1293 LlmSanitizeResponseContext::for_response_codec(response_codec),
1294 &entries,
1295 )
1296 .await;
1297 let annotation_omitted = data.as_ref().is_none_or(Json::is_null);
1298 let annotated_response = (!annotation_omitted)
1299 .then(|| {
1300 let pricing = crate::codec::response::active_pricing_resolver();
1301 finalize_optimization_summary(
1302 &handle.optimization_recorder,
1303 None,
1304 handle.model_name.as_deref(),
1305 &pricing,
1306 )
1307 })
1308 .flatten()
1309 .map(|summary| {
1310 Arc::new(AnnotatedLlmResponse {
1311 optimization_summary: Some(summary),
1312 ..AnnotatedLlmResponse::default()
1313 })
1314 });
1315 global_context()
1316 .read()
1317 .map(|state| {
1318 state.end_llm_handle(&handle, data, metadata, annotated_response)
1319 })
1320 .unwrap_or(event)
1321 })
1322 }),
1323 event_sanitizers,
1324 &subscribers,
1325 scope_stack,
1326 );
1327 drop(pending_publication);
1328 }
1329}
1330
1331pub async fn llm_call_execute(params: LlmCallExecuteParams) -> Result<Json> {
1372 let LlmCallExecuteParams {
1373 name,
1374 request,
1375 func,
1376 parent,
1377 attributes,
1378 data,
1379 metadata,
1380 model_name,
1381 codec,
1382 response_codec,
1383 } = params;
1384 ensure_runtime_owner()?;
1385 {
1386 let (entries, subscribers, parent_uuid, guardrail_metadata) = {
1387 let scope_stack = current_scope_stack();
1388 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1389 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1390 ®istries.llm_conditional_execution_guardrails
1391 });
1392 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1393 let context = global_context();
1394 let state = context
1395 .read()
1396 .map_err(|error| FlowError::Internal(error.to_string()))?;
1397 let entries = state.llm_conditional_execution_entries(&scope_locals);
1398 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1399 (
1400 entries,
1401 subscribers,
1402 resolve_parent_uuid(parent.as_ref()),
1403 metadata.clone(),
1404 )
1405 };
1406 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1407 &request,
1408 &entries,
1409 &subscribers,
1410 parent_uuid,
1411 guardrail_metadata,
1412 )
1413 .await?
1414 {
1415 let mut rejection_data = json!({});
1416 if let Some(object) = rejection_data.as_object_mut() {
1417 object.insert("rejected".into(), json!(true));
1418 object.insert("rejection_reason".into(), json!(&error));
1419 }
1420 let _ = event(
1421 EmitMarkEventParams::builder()
1422 .name(&name)
1423 .parent_opt(parent.as_ref())
1424 .data(rejection_data)
1425 .metadata_opt(metadata.clone())
1426 .build(),
1427 );
1428 return Err(FlowError::GuardrailRejected(error));
1429 }
1430 }
1431
1432 let request_codec = codec.clone();
1433 let optimization_recorder = LlmOptimizationRecorder::default();
1434 let (intercepted_request, annotated_request, pending_marks, optimization_contributions) =
1435 scope_llm_optimization_recorder(optimization_recorder.clone(), async {
1436 run_request_intercepts_with_codec_and_recorder(
1437 &name,
1438 request,
1439 codec,
1440 &optimization_recorder,
1441 )
1442 .await
1443 })
1444 .await?;
1445
1446 let mut handle = create_llm_handle(
1447 CreateLlmHandleParams::builder()
1448 .name(name.as_str())
1449 .parent_uuid_opt(resolve_parent_uuid(parent.as_ref()))
1450 .attributes(attributes)
1451 .data_opt(data.clone())
1452 .metadata_opt(metadata.clone())
1453 .model_name_opt(model_name)
1454 .build(),
1455 )?;
1456 handle.optimization_recorder = optimization_recorder;
1457 let lifecycle_subscribers = {
1458 let scope_stack = handle.captured_scope_stack();
1459 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1460 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
1461 };
1462 emit_llm_start_with_subscribers(
1463 &handle,
1464 &intercepted_request,
1465 annotated_request.clone(),
1466 request_codec.clone(),
1467 &lifecycle_subscribers,
1468 )
1469 .await?;
1470 emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?;
1471 handle
1472 .optimization_recorder
1473 .record_all(optimization_contributions);
1474 emit_optimization_marks(&handle, &lifecycle_subscribers).await;
1475
1476 let mut completion = ManagedLlmCompletion::new(
1477 &handle,
1478 metadata.clone(),
1479 response_codec.clone(),
1480 &lifecycle_subscribers,
1481 );
1482 let execution_name = name.clone();
1483 let event_uuid = handle.uuid;
1484 let execution = with_active_event_uuid(
1485 event_uuid,
1486 scope_llm_optimization_recorder(handle.optimization_recorder.clone(), async move {
1487 let execution = {
1488 let scope_stack = current_scope_stack();
1489 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1490 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1491 ®istries.llm_execution_intercepts
1492 });
1493 let context = global_context();
1494 let state = context
1495 .read()
1496 .map_err(|error| FlowError::Internal(error.to_string()))?;
1497 state.llm_build_execution_chain(&execution_name, func, &scope_locals)
1498 };
1499 execution(intercepted_request).await
1500 }),
1501 )
1502 .await;
1503
1504 match execution {
1505 Ok(response) => {
1506 llm_call_end_with_behavior(
1507 LlmCallEndParams::builder()
1508 .handle(&handle)
1509 .response(response.clone())
1510 .data_opt(data)
1511 .metadata_opt(metadata)
1512 .response_codec_opt(response_codec)
1513 .build(),
1514 LlmCallEndBehavior {
1515 response_codec_errors_fatal: false,
1516 attach_estimated_cost: true,
1517 },
1518 Some(&lifecycle_subscribers),
1519 )
1520 .await?;
1521 completion.disarm();
1522 Ok(response)
1523 }
1524 Err(error) => {
1525 let end_metadata = metadata_with_otel_error(metadata, &error);
1526 let _ = emit_llm_end_without_output(
1527 &handle,
1528 end_metadata,
1529 response_codec,
1530 Some(&lifecycle_subscribers),
1531 )
1532 .await;
1533 completion.disarm();
1534 Err(error)
1535 }
1536 }
1537}
1538
1539pub async fn llm_stream_call_execute(params: LlmStreamCallExecuteParams) -> Result<LlmJsonStream> {
1579 let LlmStreamCallExecuteParams {
1580 name,
1581 request,
1582 func,
1583 collector,
1584 finalizer,
1585 parent,
1586 attributes,
1587 data,
1588 metadata,
1589 model_name,
1590 codec,
1591 response_codec,
1592 } = params;
1593 ensure_runtime_owner()?;
1594 {
1595 let (entries, subscribers, parent_uuid, guardrail_metadata) = {
1596 let scope_stack = current_scope_stack();
1597 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1598 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1599 ®istries.llm_conditional_execution_guardrails
1600 });
1601 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1602 let context = global_context();
1603 let state = context
1604 .read()
1605 .map_err(|error| FlowError::Internal(error.to_string()))?;
1606 let entries = state.llm_conditional_execution_entries(&scope_locals);
1607 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1608 (
1609 entries,
1610 subscribers,
1611 resolve_parent_uuid(parent.as_ref()),
1612 metadata.clone(),
1613 )
1614 };
1615 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1616 &request,
1617 &entries,
1618 &subscribers,
1619 parent_uuid,
1620 guardrail_metadata,
1621 )
1622 .await?
1623 {
1624 let mut rejection_data = json!({});
1625 if let Some(object) = rejection_data.as_object_mut() {
1626 object.insert("rejected".into(), json!(true));
1627 object.insert("rejection_reason".into(), json!(&error));
1628 }
1629 let _ = event(
1630 EmitMarkEventParams::builder()
1631 .name(&name)
1632 .parent_opt(parent.as_ref())
1633 .data(rejection_data)
1634 .metadata_opt(metadata.clone())
1635 .build(),
1636 );
1637 return Err(FlowError::GuardrailRejected(error));
1638 }
1639 }
1640
1641 let request_codec = codec.clone();
1642 let optimization_recorder = LlmOptimizationRecorder::default();
1643 let (intercepted_request, annotated_request, pending_marks, optimization_contributions) =
1644 scope_llm_optimization_recorder(optimization_recorder.clone(), async {
1645 run_request_intercepts_with_codec_and_recorder(
1646 &name,
1647 request,
1648 codec,
1649 &optimization_recorder,
1650 )
1651 .await
1652 })
1653 .await?;
1654
1655 let mut handle = create_llm_handle(
1656 CreateLlmHandleParams::builder()
1657 .name(name.as_str())
1658 .parent_uuid_opt(resolve_parent_uuid(parent.as_ref()))
1659 .attributes(attributes)
1660 .data_opt(data.clone())
1661 .metadata_opt(metadata.clone())
1662 .model_name_opt(model_name)
1663 .build(),
1664 )?;
1665 handle.optimization_recorder = optimization_recorder;
1666 let lifecycle_subscribers = {
1667 let scope_stack = handle.captured_scope_stack();
1668 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1669 snapshot_event_subscribers(scope_guard.collect_scope_local_subscribers())?
1670 };
1671 emit_llm_start_with_subscribers(
1672 &handle,
1673 &intercepted_request,
1674 annotated_request,
1675 request_codec.clone(),
1676 &lifecycle_subscribers,
1677 )
1678 .await?;
1679 emit_pending_request_marks(&handle, pending_marks, &lifecycle_subscribers).await?;
1680 handle
1681 .optimization_recorder
1682 .record_all(optimization_contributions);
1683 emit_optimization_marks(&handle, &lifecycle_subscribers).await;
1684
1685 let mut completion = ManagedLlmCompletion::new(
1686 &handle,
1687 metadata.clone(),
1688 response_codec.clone(),
1689 &lifecycle_subscribers,
1690 );
1691 let execution_name = name.clone();
1692 let event_uuid = handle.uuid;
1693 let execution = with_active_event_uuid(
1694 event_uuid,
1695 scope_llm_optimization_recorder(handle.optimization_recorder.clone(), async move {
1696 let execution = {
1697 let scope_stack = current_scope_stack();
1698 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1699 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1700 ®istries.llm_stream_execution_intercepts
1701 });
1702 let context = global_context();
1703 let state = context
1704 .read()
1705 .map_err(|error| FlowError::Internal(error.to_string()))?;
1706 state.llm_stream_build_execution_chain(&execution_name, func, &scope_locals)
1707 };
1708 let execution_context = MiddlewareContinuationContext::capture();
1709 execution(intercepted_request)
1710 .await
1711 .map(|stream| contextualize_stream(stream, execution_context))
1712 }),
1713 )
1714 .await;
1715
1716 match execution {
1717 Ok(raw_stream) => {
1718 let wrapper = LlmStreamWrapper::new_managed(
1719 raw_stream,
1720 handle,
1721 collector,
1722 finalizer,
1723 metadata,
1724 response_codec,
1725 lifecycle_subscribers,
1726 );
1727 completion.disarm();
1728 Ok(LlmJsonStream::from_closeable(wrapper))
1729 }
1730 Err(error) => {
1731 let end_metadata = metadata_with_otel_error(metadata, &error);
1732 let _ = emit_llm_end_without_output(
1733 &handle,
1734 end_metadata,
1735 response_codec,
1736 Some(&lifecycle_subscribers),
1737 )
1738 .await;
1739 completion.disarm();
1740 Err(error)
1741 }
1742 }
1743}
1744
1745pub async fn llm_request_intercepts(
1769 name: &str,
1770 request: LlmRequest,
1771) -> Result<LlmRequestInterceptOutcome> {
1772 ensure_runtime_owner()?;
1773 let entries = {
1774 let scope_stack = current_scope_stack();
1775 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1776 let scope_locals = scope_guard
1777 .collect_scope_local_registries(|registries| ®istries.llm_request_intercepts);
1778 let context = global_context();
1779 let state = context
1780 .read()
1781 .map_err(|error| FlowError::Internal(error.to_string()))?;
1782 state.llm_request_intercept_entries(&scope_locals)
1783 };
1784 let mut outcome = NemoRelayContextState::llm_request_intercepts_snapshot_chain(
1785 name, request, None, &entries, false,
1786 )
1787 .await?;
1788 inject_dynamo_session_ids(&mut outcome.request);
1789 Ok(outcome)
1790}
1791
1792pub async fn llm_conditional_execution(request: &LlmRequest) -> Result<()> {
1813 ensure_runtime_owner()?;
1814 let (entries, subscribers, parent_uuid) = {
1815 let scope_stack = current_scope_stack();
1816 let scope_guard = scope_stack.read().expect("scope stack lock poisoned");
1817 let scope_locals = scope_guard.collect_scope_local_registries(|registries| {
1818 ®istries.llm_conditional_execution_guardrails
1819 });
1820 let scope_subscribers = scope_guard.collect_scope_local_subscribers();
1821 let context = global_context();
1822 let state = context
1823 .read()
1824 .map_err(|error| FlowError::Internal(error.to_string()))?;
1825 let entries = state.llm_conditional_execution_entries(&scope_locals);
1826 let subscribers = state.collect_event_subscribers(&scope_subscribers);
1827 (entries, subscribers, resolve_parent_uuid(None))
1828 };
1829 if let Some(error) = NemoRelayContextState::llm_conditional_execution_snapshot_chain(
1830 request,
1831 &entries,
1832 &subscribers,
1833 parent_uuid,
1834 None,
1835 )
1836 .await?
1837 {
1838 return Err(FlowError::GuardrailRejected(error));
1839 }
1840 Ok(())
1841}
1842
1843#[cfg(test)]
1844#[path = "../../tests/unit/llm_api_tests.rs"]
1845mod tests;