Skip to content
Merged
33 changes: 26 additions & 7 deletions crates/core/src/api/runtime/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,19 +1785,38 @@ fn validate_event_metadata_attributes(attributes: &BTreeMap<String, Json>) -> Re
Ok(())
}

fn is_otel_compatible_attribute_number(value: &serde_json::Number) -> bool {
#[derive(Clone, Copy, PartialEq, Eq)]
enum OtelAttributePrimitiveKind {
Boolean,
Integer,
Double,
String,
}

fn otel_compatible_attribute_number_kind(
Comment thread
ericevans-nv marked this conversation as resolved.
value: &serde_json::Number,
) -> Option<OtelAttributePrimitiveKind> {
if let Some(value) = value.as_u64() {
return i64::try_from(value).is_ok();
return i64::try_from(value)
.is_ok()
.then_some(OtelAttributePrimitiveKind::Integer);
}
if value.as_i64().is_some() {
return Some(OtelAttributePrimitiveKind::Integer);
}
value.as_i64().is_some() || value.as_f64().is_some()
value.as_f64().map(|_| OtelAttributePrimitiveKind::Double)
}
Comment thread
ericevans-nv marked this conversation as resolved.

fn is_otel_compatible_attribute_number(value: &serde_json::Number) -> bool {
otel_compatible_attribute_number_kind(value).is_some()
}

fn is_otel_compatible_attribute_value(value: &Json) -> bool {
fn primitive_kind(value: &Json) -> Option<u8> {
fn primitive_kind(value: &Json) -> Option<OtelAttributePrimitiveKind> {
match value {
Json::Bool(_) => Some(0),
Json::Number(value) if is_otel_compatible_attribute_number(value) => Some(1),
Json::String(_) => Some(2),
Json::Bool(_) => Some(OtelAttributePrimitiveKind::Boolean),
Json::Number(value) => otel_compatible_attribute_number_kind(value),
Json::String(_) => Some(OtelAttributePrimitiveKind::String),
_ => None,
}
}
Expand Down
7 changes: 5 additions & 2 deletions crates/core/tests/unit/runtime_state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ async fn event_metadata_injection_accepts_flat_otel_values_and_empty_output() {
),
("nv.test.strings".into(), json!(["a", "b"])),
("nv.test.booleans".into(), json!([true, false])),
("nv.test.numbers".into(), json!([1, 2])),
("nv.test.integers".into(), json!([1, 2])),
("nv.test.doubles".into(), json!([1.25, 2.5])),
("nv.test.empty".into(), json!([])),
]))
})
Expand Down Expand Up @@ -68,7 +69,8 @@ async fn event_metadata_injection_accepts_flat_otel_values_and_empty_output() {
);
assert_eq!(metadata["nv.test.strings"], json!(["a", "b"]));
assert_eq!(metadata["nv.test.booleans"], json!([true, false]));
assert_eq!(metadata["nv.test.numbers"], json!([1, 2]));
assert_eq!(metadata["nv.test.integers"], json!([1, 2]));
assert_eq!(metadata["nv.test.doubles"], json!([1.25, 2.5]));
assert_eq!(metadata["nv.test.empty"], json!([]));
}

Expand All @@ -92,6 +94,7 @@ async fn event_metadata_injection_rejects_invalid_output_atomically() {
BTreeMap::from([("nv.test.object".into(), json!({"nested": true}))]),
BTreeMap::from([("nv.test.nested_list".into(), json!([[1]]))]),
BTreeMap::from([("nv.test.mixed_list".into(), json!([1, "two"]))]),
BTreeMap::from([("nv.test.mixed_numbers".into(), json!([1, 2.5]))]),
BTreeMap::from([("nv.test.oversized_number".into(), json!(u64::MAX))]),
BTreeMap::from([("nv.test.oversized_list".into(), json!([u64::MAX]))]),
];
Expand Down
77 changes: 72 additions & 5 deletions crates/ffi/nemo_relay.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,13 @@ typedef struct Option_NemoRelayFinalizerCb Option_NemoRelayFinalizerCb;
typedef struct Option_NemoRelayPluginValidateCb Option_NemoRelayPluginValidateCb;

/**
* Callback for mark and scope event sanitizers.
* The returned JSON string transfers to Relay and is freed exactly once.
* Callback for event metadata injection.
*
* The returned string must contain a JSON object whose properties are proposed
* metadata additions. It transfers to Relay and is freed exactly once. Return
* null after setting the last error message to report a callback failure.
*/
typedef char *(*NemoRelayEventSanitizeCb)(void *user_data,
const struct FfiEvent *event,
const char *fields_json);
typedef char *(*NemoRelayEventMetadataInjectorCb)(void *user_data, const struct FfiEvent *event);

/**
* Optional destructor for user data passed to callbacks.
Expand All @@ -269,6 +270,14 @@ typedef char *(*NemoRelayEventSanitizeCb)(void *user_data,
*/
typedef void (*NemoRelayFreeFn)(void *user_data);

/**
* Callback for mark and scope event sanitizers.
* The returned JSON string transfers to Relay and is freed exactly once.
*/
typedef char *(*NemoRelayEventSanitizeCb)(void *user_data,
const struct FfiEvent *event,
const char *fields_json);

/**
* Callback for LLM execution (default callable). Receives a native JSON C string,
* returns the response as a JSON C string.
Expand Down Expand Up @@ -871,6 +880,50 @@ NemoRelayStatus nemo_relay_adaptive_build_cache_telemetry_event(const char *opti
*/
NemoRelayStatus nemo_relay_adaptive_set_latency_sensitivity(uint32_t value);

/**
* Register a global event metadata injector.
*
* # Safety
* Pointers must remain valid for the documented call lifetime. The callback
* and user data remain owned by Relay until deregistration.
*/
NemoRelayStatus nemo_relay_register_event_metadata_injector(const char *name,
int32_t priority,
NemoRelayEventMetadataInjectorCb cb,
void *user_data,
NemoRelayFreeFn free_fn);

/**
* Deregister a global event metadata injector.
*
* # Safety
* `name` must be a valid C string.
*/
NemoRelayStatus nemo_relay_deregister_event_metadata_injector(const char *name);

/**
* Register an event metadata injector owned by an active scope.
*
* # Safety
* Pointers must remain valid for the documented call lifetime. The callback
* and user data remain owned by Relay until deregistration or scope cleanup.
*/
NemoRelayStatus nemo_relay_scope_register_event_metadata_injector(const char *scope_uuid,
const char *name,
int32_t priority,
NemoRelayEventMetadataInjectorCb cb,
void *user_data,
NemoRelayFreeFn free_fn);

/**
* Deregister an event metadata injector owned by an active scope.
*
* # Safety
* String pointers must be valid C strings.
*/
NemoRelayStatus nemo_relay_scope_deregister_event_metadata_injector(const char *scope_uuid,
const char *name);

/**
* Register a global mark event sanitizer.
* # Safety
Expand Down Expand Up @@ -2057,6 +2110,20 @@ NemoRelayStatus nemo_relay_plugin_context_register_subscriber(struct FfiPluginCo
void *user_data,
NemoRelayFreeFn free_fn);

/**
* Register an event metadata injector into a plugin context.
*
* # Safety
* Pointers must remain valid for the documented call lifetime. The callback
* and user data remain owned by the plugin registration until rollback.
*/
NemoRelayStatus nemo_relay_plugin_context_register_event_metadata_injector(struct FfiPluginContext *ctx,
const char *name,
int32_t priority,
NemoRelayEventMetadataInjectorCb cb,
void *user_data,
NemoRelayFreeFn free_fn);

/**
* Register a mark event sanitizer into a plugin context.
* # Safety
Expand Down
107 changes: 105 additions & 2 deletions crates/ffi/src/api/event_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

use super::{
NemoRelayEventSanitizeCb, NemoRelayFreeFn, NemoRelayStatus, c_char, c_str_to_string,
clear_last_error, core_registry_api, set_last_error, status_from_error, wrap_event_sanitize_fn,
NemoRelayEventMetadataInjectorCb, NemoRelayEventSanitizeCb, NemoRelayFreeFn, NemoRelayStatus,
c_char, c_str_to_string, clear_last_error, core_registry_api, set_last_error,
status_from_error, wrap_event_metadata_injector_fn, wrap_event_sanitize_fn,
};

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -67,6 +68,108 @@ fn parse_scope_uuid(value: *const c_char) -> Result<uuid::Uuid, NemoRelayStatus>
})
}

/// Register a global event metadata injector.
///
/// # Safety
/// Pointers must remain valid for the documented call lifetime. The callback
/// and user data remain owned by Relay until deregistration.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nemo_relay_register_event_metadata_injector(
name: *const c_char,
priority: i32,
cb: NemoRelayEventMetadataInjectorCb,
user_data: *mut libc::c_void,
free_fn: NemoRelayFreeFn,
) -> NemoRelayStatus {
clear_last_error();
let Some(cb) = cb else {
set_last_error("event metadata injector callback is null");
return NemoRelayStatus::NullPointer;
};
let callback = wrap_event_metadata_injector_fn(cb, user_data, free_fn);
let name = match c_str_to_string(name) {
Ok(value) => value,
Err(status) => return status,
};
core_registry_api::register_event_metadata_injector(&name, priority, callback)
.map(|()| NemoRelayStatus::Ok)
.unwrap_or_else(|error| status_from_error(&error))
}

/// Deregister a global event metadata injector.
///
/// # Safety
/// `name` must be a valid C string.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nemo_relay_deregister_event_metadata_injector(
name: *const c_char,
) -> NemoRelayStatus {
clear_last_error();
let name = match c_str_to_string(name) {
Ok(value) => value,
Err(status) => return status,
};
core_registry_api::deregister_event_metadata_injector(&name)
.map(|_| NemoRelayStatus::Ok)
.unwrap_or_else(|error| status_from_error(&error))
}

/// Register an event metadata injector owned by an active scope.
///
/// # Safety
/// Pointers must remain valid for the documented call lifetime. The callback
/// and user data remain owned by Relay until deregistration or scope cleanup.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nemo_relay_scope_register_event_metadata_injector(
scope_uuid: *const c_char,
name: *const c_char,
priority: i32,
cb: NemoRelayEventMetadataInjectorCb,
user_data: *mut libc::c_void,
free_fn: NemoRelayFreeFn,
) -> NemoRelayStatus {
clear_last_error();
let Some(cb) = cb else {
set_last_error("event metadata injector callback is null");
return NemoRelayStatus::NullPointer;
};
let callback = wrap_event_metadata_injector_fn(cb, user_data, free_fn);
let uuid = match parse_scope_uuid(scope_uuid) {
Ok(value) => value,
Err(status) => return status,
};
let name = match c_str_to_string(name) {
Ok(value) => value,
Err(status) => return status,
};
core_registry_api::scope_register_event_metadata_injector(&uuid, &name, priority, callback)
.map(|()| NemoRelayStatus::Ok)
.unwrap_or_else(|error| status_from_error(&error))
}

/// Deregister an event metadata injector owned by an active scope.
///
/// # Safety
/// String pointers must be valid C strings.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nemo_relay_scope_deregister_event_metadata_injector(
scope_uuid: *const c_char,
name: *const c_char,
) -> NemoRelayStatus {
clear_last_error();
let uuid = match parse_scope_uuid(scope_uuid) {
Ok(value) => value,
Err(status) => return status,
};
let name = match c_str_to_string(name) {
Ok(value) => value,
Err(status) => return status,
};
core_registry_api::scope_deregister_event_metadata_injector(&uuid, &name)
.map(|_| NemoRelayStatus::Ok)
.unwrap_or_else(|error| status_from_error(&error))
}

unsafe fn register_scope(
scope_uuid: *const c_char,
name: *const c_char,
Expand Down
13 changes: 7 additions & 6 deletions crates/ffi/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ use std::sync::{Arc, OnceLock};
use std::time::Duration;

use crate::callable::{
NemoRelayCodecDecodeFn, NemoRelayCodecEncodeFn, NemoRelayCollectorCb, NemoRelayEventSanitizeCb,
NemoRelayEventSubscriberCb, NemoRelayFinalizerCb, NemoRelayFreeFn, NemoRelayLlmConditionalCb,
NemoRelayLlmExecCb, NemoRelayLlmExecInterceptCb, NemoRelayLlmRequestInterceptCb,
NemoRelayLlmSanitizeRequestCb, NemoRelayLlmSanitizeResponseCb, NemoRelayPluginRegisterCb,
NemoRelayPluginValidateCb, NemoRelayToolConditionalCb, NemoRelayToolExecCb,
NemoRelayToolExecInterceptCb, NemoRelayToolSanitizeCb, wrap_codec_fn, wrap_collector_fn,
NemoRelayCodecDecodeFn, NemoRelayCodecEncodeFn, NemoRelayCollectorCb,
NemoRelayEventMetadataInjectorCb, NemoRelayEventSanitizeCb, NemoRelayEventSubscriberCb,
NemoRelayFinalizerCb, NemoRelayFreeFn, NemoRelayLlmConditionalCb, NemoRelayLlmExecCb,
NemoRelayLlmExecInterceptCb, NemoRelayLlmRequestInterceptCb, NemoRelayLlmSanitizeRequestCb,
NemoRelayLlmSanitizeResponseCb, NemoRelayPluginRegisterCb, NemoRelayPluginValidateCb,
NemoRelayToolConditionalCb, NemoRelayToolExecCb, NemoRelayToolExecInterceptCb,
NemoRelayToolSanitizeCb, wrap_codec_fn, wrap_collector_fn, wrap_event_metadata_injector_fn,
wrap_event_sanitize_fn, wrap_event_subscriber, wrap_finalizer_fn, wrap_llm_conditional_fn,
wrap_llm_exec_fn, wrap_llm_exec_intercept_fn, wrap_llm_request_intercept_fn,
wrap_llm_sanitize_request_fn, wrap_llm_sanitize_response_fn, wrap_llm_stream_exec_fn,
Expand Down
63 changes: 49 additions & 14 deletions crates/ffi/src/api/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@

use super::{
Arc, CStr, ConfigDiagnostic, DiagnosticLevel, DynamicPluginActivationSpec, FfiPluginActivation,
FfiPluginContext, Future, NemoRelayEventSanitizeCb, NemoRelayEventSubscriberCb,
NemoRelayFreeFn, NemoRelayLlmConditionalCb, NemoRelayLlmExecInterceptCb,
NemoRelayLlmRequestInterceptCb, NemoRelayLlmSanitizeRequestCb, NemoRelayLlmSanitizeResponseCb,
NemoRelayPluginRegisterCb, NemoRelayPluginValidateCb, NemoRelayStatus,
NemoRelayToolConditionalCb, NemoRelayToolExecInterceptCb, NemoRelayToolSanitizeCb, Pin, Plugin,
PluginConfig, PluginError, PluginHostActivation, PluginRegistrationContext,
active_plugin_report, c_char, c_str_to_json, c_str_to_string, clear_last_error,
clear_plugin_configuration, deregister_plugin, initialize_plugins, json_to_c_string,
last_error_message, list_plugin_kinds, nemo_relay_string_free, register_adaptive_component,
register_plugin, set_last_error, status_from_plugin_error, tokio_runtime,
validate_plugin_config, wrap_event_sanitize_fn, wrap_event_subscriber, wrap_llm_conditional_fn,
wrap_llm_exec_intercept_fn, wrap_llm_request_intercept_fn, wrap_llm_sanitize_request_fn,
wrap_llm_sanitize_response_fn, wrap_llm_stream_exec_intercept_fn, wrap_tool_conditional_fn,
wrap_tool_exec_intercept_fn, wrap_tool_request_intercept_fn, wrap_tool_sanitize_fn,
FfiPluginContext, Future, NemoRelayEventMetadataInjectorCb, NemoRelayEventSanitizeCb,
NemoRelayEventSubscriberCb, NemoRelayFreeFn, NemoRelayLlmConditionalCb,
NemoRelayLlmExecInterceptCb, NemoRelayLlmRequestInterceptCb, NemoRelayLlmSanitizeRequestCb,
NemoRelayLlmSanitizeResponseCb, NemoRelayPluginRegisterCb, NemoRelayPluginValidateCb,
NemoRelayStatus, NemoRelayToolConditionalCb, NemoRelayToolExecInterceptCb,
NemoRelayToolSanitizeCb, Pin, Plugin, PluginConfig, PluginError, PluginHostActivation,
PluginRegistrationContext, active_plugin_report, c_char, c_str_to_json, c_str_to_string,
clear_last_error, clear_plugin_configuration, deregister_plugin, initialize_plugins,
json_to_c_string, last_error_message, list_plugin_kinds, nemo_relay_string_free,
register_adaptive_component, register_plugin, set_last_error, status_from_plugin_error,
tokio_runtime, validate_plugin_config, wrap_event_metadata_injector_fn, wrap_event_sanitize_fn,
wrap_event_subscriber, wrap_llm_conditional_fn, wrap_llm_exec_intercept_fn,
wrap_llm_request_intercept_fn, wrap_llm_sanitize_request_fn, wrap_llm_sanitize_response_fn,
wrap_llm_stream_exec_intercept_fn, wrap_tool_conditional_fn, wrap_tool_exec_intercept_fn,
wrap_tool_request_intercept_fn, wrap_tool_sanitize_fn,
};
use crate::api::event_registry::Surface;
use nemo_relay_pii_redaction::component::register_pii_redaction_component;
Expand Down Expand Up @@ -542,6 +543,40 @@ pub unsafe extern "C" fn nemo_relay_plugin_context_register_subscriber(
}
}

/// Register an event metadata injector into a plugin context.
///
/// # Safety
/// Pointers must remain valid for the documented call lifetime. The callback
/// and user data remain owned by the plugin registration until rollback.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn nemo_relay_plugin_context_register_event_metadata_injector(
ctx: *mut FfiPluginContext,
name: *const c_char,
priority: i32,
cb: NemoRelayEventMetadataInjectorCb,
user_data: *mut libc::c_void,
free_fn: NemoRelayFreeFn,
) -> NemoRelayStatus {
clear_last_error();
if ctx.is_null() {
set_last_error("plugin context is null");
return NemoRelayStatus::NullPointer;
}
let Some(cb) = cb else {
set_last_error("event metadata injector callback is null");
return NemoRelayStatus::NullPointer;
};
let callback = wrap_event_metadata_injector_fn(cb, user_data, free_fn);
let name = match c_str_to_string(name) {
Ok(value) => value,
Err(status) => return status,
};
match unsafe { &mut *((*ctx).0) }.register_event_metadata_injector(&name, priority, callback) {
Ok(()) => NemoRelayStatus::Ok,
Err(error) => status_from_plugin_error(&error),
}
}

unsafe fn plugin_register_event_sanitizer(
ctx: *mut FfiPluginContext,
name: *const c_char,
Expand Down
Loading
Loading