use super::extensions::InputTrigger;
use crate::protocols::{
anthropic::{
AnthropicContentBlock, AnthropicCreateMessageRequest, AnthropicMessageContent,
AnthropicRole,
},
openai::{
chat_completions::NvCreateChatCompletionRequest, completions::NvCreateCompletionRequest,
responses::NvCreateResponse,
},
};
use dynamo_protocols::types::{
ChatCompletionRequestMessage, CreateChatCompletionRequest,
responses::{
InputItem, InputParam, InputRole, Item, MessageItem, Role as ResponseRole,
ToolSearchExecutionType,
},
};
pub fn classify_chat_request(request: &NvCreateChatCompletionRequest) -> InputTrigger {
classify_create_chat_completion_request(&request.inner)
}
fn classify_create_chat_completion_request(request: &CreateChatCompletionRequest) -> InputTrigger {
let Some(last) = request.messages.last() else {
return InputTrigger::Other;
};
match last {
ChatCompletionRequestMessage::User(_) => InputTrigger::UserMessage,
ChatCompletionRequestMessage::Tool(_) | ChatCompletionRequestMessage::Function(_) => {
InputTrigger::ToolResult
}
ChatCompletionRequestMessage::Assistant(_) => InputTrigger::Other,
ChatCompletionRequestMessage::System(_) | ChatCompletionRequestMessage::Developer(_) => {
InputTrigger::Other
}
}
}
pub fn classify_response_request(request: &NvCreateResponse) -> InputTrigger {
match &request.inner.input {
InputParam::Text(_) => InputTrigger::UserMessage,
InputParam::Items(items) => {
let Some(last) = items.last() else {
return InputTrigger::Other;
};
match last {
InputItem::Item(item) if is_response_tool_output(item) => InputTrigger::ToolResult,
InputItem::Item(Item::Message(MessageItem::Input(msg))) => {
if msg.role == InputRole::User {
InputTrigger::UserMessage
} else {
InputTrigger::Other
}
}
InputItem::Item(Item::Message(MessageItem::Output(_)))
| InputItem::Item(Item::FunctionCall(_))
| InputItem::Item(Item::Reasoning(_)) => InputTrigger::Other,
InputItem::EasyMessage(easy) => match easy.role {
ResponseRole::User => InputTrigger::UserMessage,
_ => InputTrigger::Other,
},
_ => InputTrigger::Other,
}
}
}
}
fn is_response_tool_output(item: &Item) -> bool {
match item {
Item::ToolSearchOutput(output) => output.execution == Some(ToolSearchExecutionType::Client),
Item::FunctionCallOutput(_)
| Item::ComputerCallOutput(_)
| Item::LocalShellCallOutput(_)
| Item::ShellCallOutput(_)
| Item::ApplyPatchCallOutput(_)
| Item::CustomToolCallOutput(_) => true,
_ => false,
}
}
pub fn classify_anthropic_request(request: &AnthropicCreateMessageRequest) -> InputTrigger {
let Some(last) = request.messages.last() else {
return InputTrigger::Other;
};
match last.role {
AnthropicRole::User => match &last.content {
AnthropicMessageContent::Text { .. } => InputTrigger::UserMessage,
AnthropicMessageContent::Blocks { content: blocks } => {
if blocks
.iter()
.any(|b| matches!(b, AnthropicContentBlock::ToolResult { .. }))
{
InputTrigger::ToolResult
} else {
InputTrigger::UserMessage
}
}
},
AnthropicRole::Assistant | AnthropicRole::System => InputTrigger::Other,
}
}
pub fn classify_completion_request(_request: &NvCreateCompletionRequest) -> InputTrigger {
InputTrigger::Other
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocols::openai::{
chat_completions::NvCreateChatCompletionRequest, common_ext::CommonExt,
};
use dynamo_protocols::types::responses::{CreateResponse, EasyInputContent, EasyInputMessage};
use dynamo_protocols::types::{
ChatCompletionRequestMessage, ChatCompletionRequestToolMessage,
ChatCompletionRequestToolMessageContent, ChatCompletionRequestUserMessage,
ChatCompletionRequestUserMessageContent,
};
fn chat_request_with_messages(
messages: Vec<ChatCompletionRequestMessage>,
) -> NvCreateChatCompletionRequest {
NvCreateChatCompletionRequest {
inner: CreateChatCompletionRequest {
messages,
model: "test".to_string(),
..Default::default()
},
common: CommonExt::default(),
nvext: None,
chat_template_args: None,
thinking: None,
media_io_kwargs: None,
return_tokens_as_token_ids: None,
unsupported_fields: Default::default(),
}
}
#[test]
fn chat_user_message() {
let req = chat_request_with_messages(vec![ChatCompletionRequestMessage::User(
ChatCompletionRequestUserMessage {
content: ChatCompletionRequestUserMessageContent::Text("hello".into()),
name: None,
},
)]);
assert_eq!(classify_chat_request(&req), InputTrigger::UserMessage);
}
#[test]
fn chat_tool_result() {
let req = chat_request_with_messages(vec![ChatCompletionRequestMessage::Tool(
ChatCompletionRequestToolMessage {
content: ChatCompletionRequestToolMessageContent::Text("42".into()),
tool_call_id: "call-1".into(),
},
)]);
assert_eq!(classify_chat_request(&req), InputTrigger::ToolResult);
}
#[test]
fn chat_other_empty_messages() {
let req = chat_request_with_messages(vec![]);
assert_eq!(classify_chat_request(&req), InputTrigger::Other);
}
fn response_request_with_easy_messages(role: ResponseRole) -> NvCreateResponse {
NvCreateResponse {
inner: CreateResponse {
input: InputParam::Items(vec![InputItem::EasyMessage(EasyInputMessage {
role,
content: EasyInputContent::Text("hi".into()),
..Default::default()
})]),
model: Some("test".into()),
..Default::default()
},
nvext: None,
chat_template_args: None,
}
}
fn response_request_with_item(item: serde_json::Value) -> NvCreateResponse {
NvCreateResponse {
inner: CreateResponse {
input: InputParam::Items(vec![serde_json::from_value(item).unwrap()]),
model: Some("test".into()),
..Default::default()
},
nvext: None,
chat_template_args: None,
}
}
#[test]
fn responses_easy_message_roles() {
for (role, expected) in [
(ResponseRole::User, InputTrigger::UserMessage),
(ResponseRole::Assistant, InputTrigger::Other),
(ResponseRole::System, InputTrigger::Other),
] {
assert_eq!(
classify_response_request(&response_request_with_easy_messages(role)),
expected
);
}
}
#[test]
fn responses_tool_outputs() {
for item in [
serde_json::json!({
"type": "function_call_output",
"call_id": "function-1",
"output": "ok",
}),
serde_json::json!({
"type": "tool_search_output",
"execution": "client",
"tools": [],
}),
serde_json::json!({
"type": "computer_call_output",
"call_id": "computer-1",
"output": {
"type": "computer_screenshot",
"image_url": "https://example.com/screenshot.png",
},
}),
serde_json::json!({
"type": "local_shell_call_output",
"id": "shell-1",
"output": "done",
}),
serde_json::json!({
"type": "shell_call_output",
"call_id": "shell-2",
"output": [],
}),
serde_json::json!({
"type": "apply_patch_call_output",
"call_id": "patch-1",
"status": "completed",
}),
serde_json::json!({
"type": "custom_tool_call_output",
"call_id": "custom-1",
"output": "done",
}),
] {
assert_eq!(
classify_response_request(&response_request_with_item(item)),
InputTrigger::ToolResult
);
}
}
#[test]
fn responses_hosted_or_unspecified_tool_search_outputs_are_not_tool_results() {
for item in [
serde_json::json!({
"type": "tool_search_output",
"execution": "server",
"tools": [],
}),
serde_json::json!({
"type": "tool_search_output",
"tools": [],
}),
] {
assert_eq!(
classify_response_request(&response_request_with_item(item)),
InputTrigger::Other
);
}
}
#[test]
fn responses_tool_calls_and_approval_responses_are_not_tool_outputs() {
for item in [
serde_json::json!({
"type": "function_call",
"arguments": "{}",
"call_id": "function-1",
"name": "get_weather",
}),
serde_json::json!({
"type": "mcp_approval_response",
"approval_request_id": "approval-1",
"approve": true,
}),
] {
assert_eq!(
classify_response_request(&response_request_with_item(item)),
InputTrigger::Other
);
}
}
}