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
44 changes: 1 addition & 43 deletions crates/core/src/api/runtime/callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::codec::request::AnnotatedLlmRequest;
use crate::codec::traits::{LlmCodec, LlmResponseCodec};
use crate::error::Result;
use crate::json::Json;
pub use nemo_relay_types::codec::identity::{BuiltinLlmCodec, LlmCodecIdentity};

/// Sanitize mutable observability fields on a fully constructed event.
///
Expand Down Expand Up @@ -151,49 +152,6 @@ pub(crate) type ToolExecutionOutcomeNextFn = Arc<
+ Sync,
>;

/// Relay's built-in LLM codec identities.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinLlmCodec {
/// OpenAI Chat Completions request and response payloads.
OpenAiChat,
/// OpenAI Responses request and response payloads.
OpenAiResponses,
/// Anthropic Messages request and response payloads.
AnthropicMessages,
/// OCI Generative AI chat request and response payloads.
OCIGenAI,
/// Gemini generateContent request and response payloads.
GeminiGenerateContent,
}

impl BuiltinLlmCodec {
/// Stable identifier used in configuration and language bindings.
#[must_use]
pub const fn id(self) -> &'static str {
match self {
Self::OpenAiChat => "openai_chat",
Self::OpenAiResponses => "openai_responses",
Self::AnthropicMessages => "anthropic_messages",
Self::OCIGenAI => "oci_genai",
Self::GeminiGenerateContent => "gemini_generate_content",
}
}
}

/// Per-call LLM codec identity supplied to sanitize guardrails.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LlmCodecIdentity {
/// No codec was active for this payload direction.
#[default]
None,
/// A Relay built-in codec was active.
BuiltIn(BuiltinLlmCodec),
/// A runtime-registered codec was active, identified by its stable ID.
Runtime(String),
/// A codec was active but does not expose a registered identity.
Opaque,
}

/// Per-call codec context for LLM request sanitize guardrails.
///
/// The context distinguishes no codec, Relay built-ins, runtime-registered
Expand Down
17 changes: 3 additions & 14 deletions crates/plugin/src/async_sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1039,20 +1039,9 @@ impl CodecIdentityInvocation {
match (self.codec_kind.as_str(), self.codec_id) {
("none", _) => Ok(LlmCodecIdentity::None),
("opaque", _) => Ok(LlmCodecIdentity::Opaque),
("builtin", Some(id)) => match id.as_str() {
"openai_chat" => Ok(LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat)),
"openai_responses" => {
Ok(LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiResponses))
}
"anthropic_messages" => Ok(LlmCodecIdentity::BuiltIn(
BuiltinLlmCodec::AnthropicMessages,
)),
"oci_genai" => Ok(LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OCIGenAI)),
"gemini_generate_content" => Ok(LlmCodecIdentity::BuiltIn(
BuiltinLlmCodec::GeminiGenerateContent,
)),
_ => Err(format!("unknown built-in LLM codec: {id}")),
},
("builtin", Some(id)) => BuiltinLlmCodec::from_id(&id)
.map(LlmCodecIdentity::BuiltIn)
.ok_or_else(|| format!("unknown built-in LLM codec: {id}")),
("runtime", Some(id)) => Ok(LlmCodecIdentity::Runtime(id)),
(kind, _) => Err(format!("invalid LLM codec context: {kind}")),
}
Expand Down
38 changes: 1 addition & 37 deletions crates/plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub use nemo_relay_types::api::event::{
pub use nemo_relay_types::api::llm::{LlmAttributes, LlmRequest, LlmRequestInterceptOutcome};
pub use nemo_relay_types::api::scope::{HandleAttributes, ScopeAttributes, ScopeType};
pub use nemo_relay_types::api::tool::{ToolAttributes, ToolExecutionInterceptOutcome};
pub use nemo_relay_types::codec::identity::{BuiltinLlmCodec, LlmCodecIdentity};
pub use nemo_relay_types::codec::optimization::{
LlmOptimizationContribution, LlmOptimizationEvidenceQuality, LlmOptimizationKind,
LlmOptimizationModel, LlmOptimizationModelTransition, LlmOptimizationPayload,
Expand All @@ -52,43 +53,6 @@ pub const NEMO_RELAY_NATIVE_ABI_VERSION_TYPED_ASYNC: u32 = 4;
/// Legacy native plugin ABI accepted by Relay hosts for compatibility.
pub const NEMO_RELAY_NATIVE_ABI_VERSION_LEGACY: u32 = 2;

/// Built-in LLM codec identities available to native plugins.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BuiltinLlmCodec {
/// OpenAI Chat Completions.
#[serde(rename = "openai_chat")]
OpenAiChat,
/// OpenAI Responses.
#[serde(rename = "openai_responses")]
OpenAiResponses,
/// Anthropic Messages.
#[serde(rename = "anthropic_messages")]
AnthropicMessages,
/// OCI Generative AI chat.
#[serde(rename = "oci_genai")]
OCIGenAI,
/// Gemini generateContent.
#[serde(rename = "gemini_generate_content")]
GeminiGenerateContent,
}

/// Per-call LLM codec identity delivered to native plugins.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, serde::Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
pub enum LlmCodecIdentity {
/// No codec was active.
#[default]
None,
/// A Relay built-in codec was active.
#[serde(rename = "builtin")]
BuiltIn(BuiltinLlmCodec),
/// A runtime-registered codec was active, identified by its stable ID.
Runtime(String),
/// A codec was active but has no registered identity.
Opaque,
}

/// Per-call request codec context delivered to an LLM sanitizer.
pub struct LlmSanitizeRequestContext<'a> {
/// Identity of the active codec.
Expand Down
79 changes: 79 additions & 0 deletions crates/plugin/tests/typed_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3368,6 +3368,85 @@ fn typed_async_llm_sanitize_context_decodes_oci_genai_builtin_identity() {
}
}

#[test]
fn typed_async_llm_sanitize_context_decodes_all_builtin_identities() {
let _guard = begin_test();

for expected in [
BuiltinLlmCodec::OpenAiChat,
BuiltinLlmCodec::OpenAiResponses,
BuiltinLlmCodec::AnthropicMessages,
BuiltinLlmCodec::OCIGenAI,
BuiltinLlmCodec::GeminiGenerateContent,
] {
let host = test_host_v4();
let mut ctx = test_context(&host.v3.v1);
ctx.register_llm_sanitize_request_guardrail(
"llm-request-builtins",
0,
move |request, context| async move {
assert_eq!(context.codec, LlmCodecIdentity::BuiltIn(expected));
Ok(Some(request))
},
)
.unwrap();

let registration =
take_async_registration(NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest);
assert_eq!(
invoke_async_registration(
&host,
&registration,
json!({
"request": test_llm_request(),
"context": { "codec_kind": "builtin", "codec_id": expected.id() }
}),
None,
)
.unwrap()["content"],
json!({ "prompt": "hello" })
);
unsafe { registration.free() };

let deadline = Instant::now() + Duration::from_secs(5);
while live_host_strings() != 0 {
assert!(
Instant::now() < deadline,
"host strings were not released after the sanitize invocation"
);
std::thread::yield_now();
}
}
}

#[test]
fn typed_async_llm_sanitize_context_rejects_unknown_builtin_identity() {
let _guard = begin_test();
let host = test_host_v4();
let mut ctx = test_context(&host.v3.v1);
ctx.register_llm_sanitize_request_guardrail(
"llm-request-unknown",
0,
|request, _context| async move { Ok(Some(request)) },
)
.unwrap();

let registration =
take_async_registration(NemoRelayNativeAsyncMiddlewareKind::LlmSanitizeRequest);
let error = invoke_async_registration(
&host,
&registration,
json!({
"request": test_llm_request(),
"context": { "codec_kind": "builtin", "codec_id": "future_provider" }
}),
None,
)
.expect_err("unknown built-in codec identities must be rejected");
assert!(error.contains("unknown built-in LLM codec: future_provider"));
unsafe { registration.free() };
}

#[test]
fn typed_async_registration_failure_rolls_back_callback_state() {
let _guard = begin_test();
Expand Down
74 changes: 74 additions & 0 deletions crates/types/src/codec/identity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Stable LLM codec identities shared across Relay runtime and SDK boundaries.
//!
//! These types identify a codec selected by the Relay runtime. They do not
//! provide codec implementations, provider detection, or runtime capabilities.

use serde::{Deserialize, Serialize};

macro_rules! builtin_llm_codecs {
($( $(#[$meta:meta])* $variant:ident => $id:literal, )+) => {
/// Relay's built-in LLM codec identities.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BuiltinLlmCodec {
$(
$(#[$meta])*
#[serde(rename = $id)]
$variant,
)+
}

impl BuiltinLlmCodec {
/// Every built-in codec identity in stable ID order.
pub const ALL: &'static [Self] = &[$(Self::$variant),+];

/// Stable identifier used in configuration and SDK transport boundaries.
#[must_use]
pub const fn id(self) -> &'static str {
match self {
$(Self::$variant => $id),+
}
}

/// Resolve a stable built-in codec identifier.
#[must_use]
pub fn from_id(id: &str) -> Option<Self> {
match id {
$($id => Some(Self::$variant),)+
_ => None,
}
}
}
};
}

builtin_llm_codecs! {
/// OpenAI Chat Completions request and response payloads.
OpenAiChat => "openai_chat",
/// OpenAI Responses request and response payloads.
OpenAiResponses => "openai_responses",
/// Anthropic Messages request and response payloads.
AnthropicMessages => "anthropic_messages",
/// OCI Generative AI chat request and response payloads.
OCIGenAI => "oci_genai",
/// Gemini generateContent request and response payloads.
GeminiGenerateContent => "gemini_generate_content",
}

/// Per-call LLM codec identity supplied to sanitizer and SDK callbacks.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(tag = "kind", content = "id", rename_all = "snake_case")]
pub enum LlmCodecIdentity {
/// No codec was active for this payload direction.
#[default]
None,
/// A Relay built-in codec was active.
#[serde(rename = "builtin")]
BuiltIn(BuiltinLlmCodec),
/// A runtime-registered codec was active, identified by its stable ID.
Runtime(String),
/// A codec was active but does not expose a registered identity.
Opaque,
}
2 changes: 2 additions & 0 deletions crates/types/src/codec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

//! Shared normalized LLM request and response data types.

/// Stable LLM codec identities shared across SDK boundaries.
pub mod identity;
/// Plugin-neutral LLM optimization evidence and summaries.
pub mod optimization;
/// Normalized LLM request data types.
Expand Down
46 changes: 46 additions & 0 deletions crates/types/tests/codec_identity_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Compatibility tests for shared LLM codec identities.

use nemo_relay_types::codec::identity::{BuiltinLlmCodec, LlmCodecIdentity};
use serde_json::json;

#[test]
fn builtin_codec_ids_round_trip() {
for &codec in BuiltinLlmCodec::ALL {
assert_eq!(BuiltinLlmCodec::from_id(codec.id()), Some(codec));
assert_eq!(serde_json::to_value(codec).unwrap(), json!(codec.id()));
}
}

#[test]
fn builtin_codec_ids_reject_unknown_values() {
for id in ["", "openai-chat", "OpenAI_chat", "unknown"] {
assert_eq!(BuiltinLlmCodec::from_id(id), None);
}
}

#[test]
fn codec_identity_preserves_the_native_plugin_json_contract() {
let cases = [
(LlmCodecIdentity::None, json!({"kind": "none"})),
(
LlmCodecIdentity::BuiltIn(BuiltinLlmCodec::OpenAiChat),
json!({"kind": "builtin", "id": "openai_chat"}),
),
(
LlmCodecIdentity::Runtime("com.example.chat.v1".to_owned()),
json!({"kind": "runtime", "id": "com.example.chat.v1"}),
),
(LlmCodecIdentity::Opaque, json!({"kind": "opaque"})),
];

for (identity, expected) in cases {
assert_eq!(serde_json::to_value(&identity).unwrap(), expected);
assert_eq!(
serde_json::from_value::<LlmCodecIdentity>(expected).unwrap(),
identity
);
}
}
Loading
Loading