Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions crates/core/src/api/runtime/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1788,8 +1788,7 @@ fn validate_event_metadata_attributes(attributes: &BTreeMap<String, Json>) -> Re
#[derive(Clone, Copy, PartialEq, Eq)]
enum OtelAttributePrimitiveKind {
Boolean,
Integer,
Double,
Number,
String,
}

Expand All @@ -1799,12 +1798,12 @@ fn otel_compatible_attribute_number_kind(
if let Some(value) = value.as_u64() {
return i64::try_from(value)
.is_ok()
.then_some(OtelAttributePrimitiveKind::Integer);
.then_some(OtelAttributePrimitiveKind::Number);
}
if value.as_i64().is_some() {
return Some(OtelAttributePrimitiveKind::Integer);
return Some(OtelAttributePrimitiveKind::Number);
}
value.as_f64().map(|_| OtelAttributePrimitiveKind::Double)
value.as_f64().map(|_| OtelAttributePrimitiveKind::Number)
}

fn is_otel_compatible_attribute_number(value: &serde_json::Number) -> bool {
Expand Down
3 changes: 2 additions & 1 deletion crates/core/tests/unit/runtime_state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async fn event_metadata_injection_accepts_flat_otel_values_and_empty_output() {
("nv.test.booleans".into(), json!([true, false])),
("nv.test.integers".into(), json!([1, 2])),
("nv.test.doubles".into(), json!([1.0, 2.5])),
("nv.test.numbers".into(), json!([1, 2.5])),
("nv.test.empty".into(), json!([])),
]))
})
Expand Down Expand Up @@ -71,6 +72,7 @@ async fn event_metadata_injection_accepts_flat_otel_values_and_empty_output() {
assert_eq!(metadata["nv.test.booleans"], json!([true, false]));
assert_eq!(metadata["nv.test.integers"], json!([1, 2]));
assert_eq!(metadata["nv.test.doubles"], json!([1.0, 2.5]));
assert_eq!(metadata["nv.test.numbers"], json!([1, 2.5]));
assert_eq!(metadata["nv.test.empty"], json!([]));
}

Expand All @@ -94,7 +96,6 @@ 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