use router_protocol::{
CallId, CanonicalRequest, CapabilityRequirements, ContentBlock, FunctionTool,
HarnessInstanceId, InputItem, M1_WIRE_VERSION, MessageRole, MultiAgentNamespace,
ReasoningIntent, SessionId, StructuredOutputIntent, ToolChoice, TurnId, validate_tool_name,
validate_tool_namespace,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use std::collections::{BTreeMap, HashSet};
use thiserror::Error;
const TOP_LEVEL_FIELDS: &[&str] = &[
"model",
"instructions",
"input",
"tools",
"tool_choice",
"parallel_tool_calls",
"reasoning",
"store",
"stream",
"stream_options",
"include",
"service_tier",
"prompt_cache_key",
"text",
"client_metadata",
];
const ITEM_FIELDS: &[&str] = &[
"type",
"id",
"role",
"content",
"tools",
"phase",
"name",
"namespace",
"arguments",
"call_id",
"output",
"summary",
"encrypted_content",
"internal_chat_message_metadata_passthrough",
];
const TOOL_FIELDS: &[&str] = &[
"type",
"name",
"description",
"strict",
"defer_loading",
"parameters",
];
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CodexRequestIdentity {
pub session_id: Option<String>,
pub thread_id: Option<String>,
pub fallback_id: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CodexWireErrorKind {
InvalidRequest,
UnsupportedCapability,
UnsupportedWireCombination,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CodexWireError {
pub kind: CodexWireErrorKind,
pub diagnostics: Value,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FailureStage {
RequestShape,
RequestStream,
RequestStore,
InputItemType,
InputItemShape,
InputFunctionArguments,
InputFunctionCallId,
InputFunctionOutput,
ContentType,
InputItemNamespace,
ToolTypeCustom,
ToolTypeToolSearch,
ToolTypeWebSearch,
ToolTypeUnknown,
ToolName,
ToolNamespaceName,
ToolNamespaceShape,
ToolNamespaceInnerType,
ToolDuplicate,
ToolReservedCollision,
ToolDeferLoading,
ToolChoice,
Reasoning,
ReasoningEffort,
ReasoningContext,
ReasoningSummary,
Text,
TextVerbosity,
TextFormat,
TextFormatType,
StreamOptions,
Include,
ServiceTier,
CanonicalValidation,
}
impl FailureStage {
#[cfg(test)]
const ALL: [Self; 34] = [
Self::RequestShape,
Self::RequestStream,
Self::RequestStore,
Self::InputItemType,
Self::InputItemShape,
Self::InputFunctionArguments,
Self::InputFunctionCallId,
Self::InputFunctionOutput,
Self::ContentType,
Self::InputItemNamespace,
Self::ToolTypeCustom,
Self::ToolTypeToolSearch,
Self::ToolTypeWebSearch,
Self::ToolTypeUnknown,
Self::ToolName,
Self::ToolNamespaceName,
Self::ToolNamespaceShape,
Self::ToolNamespaceInnerType,
Self::ToolDuplicate,
Self::ToolReservedCollision,
Self::ToolDeferLoading,
Self::ToolChoice,
Self::Reasoning,
Self::ReasoningEffort,
Self::ReasoningContext,
Self::ReasoningSummary,
Self::Text,
Self::TextVerbosity,
Self::TextFormat,
Self::TextFormatType,
Self::StreamOptions,
Self::Include,
Self::ServiceTier,
Self::CanonicalValidation,
];
const fn as_str(self) -> &'static str {
match self {
Self::RequestShape => "codex_wire.request.shape",
Self::RequestStream => "codex_wire.request.stream",
Self::RequestStore => "codex_wire.request.store",
Self::InputItemType => "codex_wire.input_item.type",
Self::InputItemShape => "codex_wire.input_item.shape",
Self::InputFunctionArguments => "codex_wire.input_function.arguments",
Self::InputFunctionCallId => "codex_wire.input_function.call_id",
Self::InputFunctionOutput => "codex_wire.input_function.output",
Self::ContentType => "codex_wire.content.type",
Self::InputItemNamespace => "codex_wire.input_item.namespace",
Self::ToolTypeCustom => "codex_wire.tool.type_custom",
Self::ToolTypeToolSearch => "codex_wire.tool.type_tool_search",
Self::ToolTypeWebSearch => "codex_wire.tool.type_web_search",
Self::ToolTypeUnknown => "codex_wire.tool.type_unknown",
Self::ToolName => "codex_wire.tool.name",
Self::ToolNamespaceName => "codex_wire.tool.namespace.name",
Self::ToolNamespaceShape => "codex_wire.tool.namespace.shape",
Self::ToolNamespaceInnerType => "codex_wire.tool.namespace.inner_type",
Self::ToolDuplicate => "codex_wire.tool.duplicate",
Self::ToolReservedCollision => "codex_wire.tool.reserved_collision",
Self::ToolDeferLoading => "codex_wire.tool.defer_loading",
Self::ToolChoice => "codex_wire.tool_choice",
Self::Reasoning => "codex_wire.reasoning",
Self::ReasoningEffort => "codex_wire.reasoning.effort",
Self::ReasoningContext => "codex_wire.reasoning.context",
Self::ReasoningSummary => "codex_wire.reasoning.summary",
Self::Text => "codex_wire.text",
Self::TextVerbosity => "codex_wire.text.verbosity",
Self::TextFormat => "codex_wire.text.format",
Self::TextFormatType => "codex_wire.text.format.type",
Self::StreamOptions => "codex_wire.stream_options.reasoning_summary_delivery",
Self::Include => "codex_wire.include",
Self::ServiceTier => "codex_wire.service_tier",
Self::CanonicalValidation => "codex_wire.canonical_validation",
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ResponsesApiRequest {
model: String,
#[serde(default)]
instructions: String,
input: Vec<Value>,
#[serde(default)]
tools: Option<Vec<Value>>,
#[serde(default = "default_tool_choice")]
tool_choice: String,
#[serde(default)]
parallel_tool_calls: bool,
#[serde(default)]
reasoning: Option<Value>,
#[serde(default)]
store: bool,
stream: bool,
#[serde(default)]
stream_options: Option<Value>,
#[serde(default)]
include: Vec<String>,
#[serde(default)]
service_tier: Option<String>,
#[serde(default)]
prompt_cache_key: Option<String>,
#[serde(default)]
text: Option<Value>,
#[serde(default)]
client_metadata: Option<BTreeMap<String, String>>,
}
fn default_tool_choice() -> String {
"auto".to_owned()
}
pub const MULTI_AGENT_NAMESPACE: &str = "multi_agent_v1";
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct CodexSubagentRequestContext {
pub max_threads: u8,
pub max_depth: u8,
pub fork_turns: &'static str,
}
impl CodexSubagentRequestContext {
pub const MAX_THREADS: u8 = 4;
pub const MAX_DEPTH: u8 = 2;
pub const FORK_TURNS: &'static str = "none";
const fn fixed() -> Self {
Self {
max_threads: Self::MAX_THREADS,
max_depth: Self::MAX_DEPTH,
fork_turns: Self::FORK_TURNS,
}
}
}
pub fn canonical_subagent_request_context(
request: &CanonicalRequest,
) -> Option<CodexSubagentRequestContext> {
let declares_namespace = request.tools.iter().any(|tool| {
tool.namespace.as_deref().is_some_and(is_subagent_namespace)
|| (tool.namespace.is_none()
&& (tool.name == MULTI_AGENT_NAMESPACE || is_mav2_tool_name(&tool.name)))
});
let continues_namespace = request.input.iter().any(|item| {
matches!(
item,
InputItem::FunctionCall {
namespace: Some(namespace),
..
} if is_subagent_namespace(namespace)
) || matches!(
item,
InputItem::FunctionCall {
namespace: None,
name,
..
} if name == MULTI_AGENT_NAMESPACE || is_mav2_tool_name(name)
)
});
(declares_namespace || continues_namespace).then_some(CodexSubagentRequestContext::fixed())
}
const MULTI_AGENT_DEFAULT_FUNCTION: &str = "spawn_agent";
fn disambiguate_multi_agent_function(arguments: &Value) -> &'static str {
let object = arguments.as_object();
let has = |key: &str| object.is_some_and(|map| map.contains_key(key));
if has("id") {
return "resume_agent";
}
if has("timeout_ms") {
return "wait_agent";
}
if has("target") {
if has("message") || has("items") || has("interrupt") {
return "send_input";
}
return "close_agent";
}
MULTI_AGENT_DEFAULT_FUNCTION
}
pub fn normalize_provider_tool_call(name: &str, arguments: &Value) -> (Option<String>, String) {
if let Some((namespace, function)) = name.split_once("__")
&& validate_tool_namespace(namespace).is_ok()
&& !function.is_empty()
{
return (Some(namespace.to_owned()), function.to_owned());
}
if name == MULTI_AGENT_NAMESPACE {
return (
Some(MULTI_AGENT_NAMESPACE.to_owned()),
disambiguate_multi_agent_function(arguments).to_owned(),
);
}
(None, name.to_owned())
}
pub const MAV2_COLLABORATION_NAMESPACE: &str = "collaboration";
pub const MAV2_AGENTS_NAMESPACE: &str = "agents";
pub const MAV2_TOOL_NAMES: [&str; 5] = [
"spawn_agent",
"send_input",
"resume_agent",
"wait_agent",
"close_agent",
];
pub fn is_subagent_namespace(namespace: &str) -> bool {
matches!(
namespace,
MULTI_AGENT_NAMESPACE | MAV2_COLLABORATION_NAMESPACE | MAV2_AGENTS_NAMESPACE
)
}
pub fn is_mav2_tool_name(name: &str) -> bool {
MAV2_TOOL_NAMES.contains(&name)
}
pub fn normalize_mav2_provider_tool_call(
name: &str,
arguments: &Value,
namespace_choice: MultiAgentNamespace,
) -> (Option<String>, String) {
if MAV2_TOOL_NAMES.contains(&name) {
return (Some(namespace_choice.as_str().to_owned()), name.to_owned());
}
normalize_provider_tool_call(name, arguments)
}
pub const NAMESPACE_ID_SMUGGLE_MARKER: &str = "__ns__";
#[derive(Debug, Error, PartialEq, Eq)]
pub enum NamespaceIdSmuggleError {
#[error("the call id collides with the namespace marker")]
MarkerCollision,
}
pub fn encode_namespace_call_id(
raw: &str,
namespace: &str,
) -> Result<String, NamespaceIdSmuggleError> {
if decode_namespace_call_id(raw).is_some() {
return Ok(raw.to_owned());
}
if raw.contains(NAMESPACE_ID_SMUGGLE_MARKER) {
return Err(NamespaceIdSmuggleError::MarkerCollision);
}
use base64::Engine;
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(namespace.as_bytes());
Ok(format!("{raw}{NAMESPACE_ID_SMUGGLE_MARKER}{payload}"))
}
pub fn decode_namespace_call_id(value: &str) -> Option<(String, String)> {
use base64::Engine;
let (raw, payload) = value.rsplit_once(NAMESPACE_ID_SMUGGLE_MARKER)?;
if raw.is_empty() {
return None;
}
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload)
.ok()?;
let namespace = String::from_utf8(bytes).ok()?;
is_subagent_namespace(&namespace).then_some((raw.to_owned(), namespace))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mav2SpawnAgentContract {
pub task_name: String,
pub model: Option<String>,
pub reasoning_effort: Option<String>,
pub fork_turns: Mav2ForkTurns,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Mav2ForkTurns {
None,
All,
Turns(u64),
}
impl Mav2ForkTurns {
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::All => "all",
Self::Turns(_) => "int",
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum Mav2SpawnAgentError {
#[error("spawn_agent arguments must be a JSON object")]
InvalidArguments,
#[error("fork_context is rejected in MAv2; use fork_turns instead")]
ForkContextRejected,
#[error("fork_turns must be none, all, or a positive integer")]
InvalidForkTurns,
#[error("model override is not a valid model identifier")]
InvalidModelOverride,
#[error("reasoning_effort override is not a supported value")]
InvalidReasoningEffortOverride,
}
const MAV2_REASONING_EFFORT_VALUES: [&str; 6] =
["none", "minimal", "low", "medium", "high", "xhigh"];
pub fn validate_mav2_spawn_agent_arguments(
arguments: &Value,
) -> Result<Mav2SpawnAgentContract, Mav2SpawnAgentError> {
let object = arguments
.as_object()
.ok_or(Mav2SpawnAgentError::InvalidArguments)?;
if object.contains_key("fork_context") {
return Err(Mav2SpawnAgentError::ForkContextRejected);
}
let fork_turns = match object.get("fork_turns") {
None | Some(Value::Null) => Mav2ForkTurns::None,
Some(Value::String(value)) if value == "none" => Mav2ForkTurns::None,
Some(Value::String(value)) if value == "all" => Mav2ForkTurns::All,
Some(Value::Number(value)) => value
.as_u64()
.filter(|turns| *turns > 0)
.map(Mav2ForkTurns::Turns)
.ok_or(Mav2SpawnAgentError::InvalidForkTurns)?,
Some(Value::String(_)) => return Err(Mav2SpawnAgentError::InvalidForkTurns),
Some(Value::Bool(_)) | Some(Value::Array(_)) | Some(Value::Object(_)) => {
return Err(Mav2SpawnAgentError::InvalidForkTurns);
}
};
let model = match object.get("model") {
None | Some(Value::Null) => None,
Some(Value::String(value)) if valid_override_model(value) => Some(value.clone()),
Some(Value::String(_)) => return Err(Mav2SpawnAgentError::InvalidModelOverride),
Some(_) => return Err(Mav2SpawnAgentError::InvalidModelOverride),
};
let reasoning_effort = match object.get("reasoning_effort") {
None | Some(Value::Null) => None,
Some(Value::String(value)) if MAV2_REASONING_EFFORT_VALUES.contains(&value.as_str()) => {
Some(value.clone())
}
Some(Value::String(_)) => {
return Err(Mav2SpawnAgentError::InvalidReasoningEffortOverride);
}
Some(_) => return Err(Mav2SpawnAgentError::InvalidReasoningEffortOverride),
};
let task_name = object
.get("task_name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
Ok(Mav2SpawnAgentContract {
task_name,
model,
reasoning_effort,
fork_turns,
})
}
fn valid_override_model(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 128
&& value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'/')
})
}
pub fn normalize_responses_request(
body: &[u8],
identity: CodexRequestIdentity,
) -> Result<CanonicalRequest, CodexWireError> {
let raw: Value = serde_json::from_slice(body).map_err(|_| CodexWireError {
kind: CodexWireErrorKind::InvalidRequest,
diagnostics: json!({
"schema": "codex_responses_0_145",
"body_type": "invalid_json"
}),
})?;
let diagnostics = structural_diagnostics(&raw);
let wire: ResponsesApiRequest = serde_json::from_value(raw)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::RequestShape))?;
normalize_wire(wire, identity, diagnostics)
}
fn normalize_wire(
wire: ResponsesApiRequest,
identity: CodexRequestIdentity,
diagnostics: Value,
) -> Result<CanonicalRequest, CodexWireError> {
if !wire.stream {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedWireCombination,
diagnostics,
FailureStage::RequestStream,
));
}
if wire.store {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedWireCombination,
diagnostics,
FailureStage::RequestStore,
));
}
validate_stream_options(wire.stream_options.as_ref(), &diagnostics)?;
validate_include(&wire.include, &diagnostics)?;
validate_service_tier(wire.service_tier.as_deref(), &diagnostics)?;
let _cache_hint_was_supplied = wire.prompt_cache_key.is_some();
let _parallel_calls_permitted = wire.parallel_tool_calls;
let metadata = wire.client_metadata.as_ref();
let session = identity
.session_id
.as_deref()
.or_else(|| metadata.and_then(|fields| fields.get("session_id").map(String::as_str)))
.or(identity.thread_id.as_deref())
.or_else(|| metadata.and_then(|fields| fields.get("thread_id").map(String::as_str)))
.unwrap_or(identity.fallback_id.as_str());
let turn = metadata
.and_then(|fields| fields.get("turn_id").map(String::as_str))
.or(identity.thread_id.as_deref())
.or_else(|| metadata.and_then(|fields| fields.get("thread_id").map(String::as_str)))
.unwrap_or(session);
let harness_instance =
HarnessInstanceId::parse("codex-cli").map_err(|_| invalid(diagnostics.clone()))?;
let session_id = SessionId::parse(session).map_err(|_| invalid(diagnostics.clone()))?;
let turn_id = TurnId::parse(turn).map_err(|_| invalid(diagnostics.clone()))?;
let mut capabilities = CapabilityRequirements::default();
let mut input = Vec::new();
let mut declared_tools = wire.tools.unwrap_or_default();
if !wire.instructions.is_empty() {
input.push(InputItem::Message {
role: MessageRole::Developer,
content: vec![ContentBlock::Text {
text: wire.instructions,
}],
});
}
for (input_index, item) in wire.input.into_iter().enumerate() {
if item
.as_object()
.and_then(|object| object.get("type"))
.and_then(Value::as_str)
== Some("additional_tools")
{
declared_tools.extend(
extract_additional_tools(&item, &diagnostics)
.map_err(|error| with_input_index(error, input_index))?,
);
continue;
}
input.push(
normalize_input_item(&item, &mut capabilities, &diagnostics)
.map_err(|error| with_input_index(error, input_index))?,
);
}
let tools = normalize_tools(declared_tools, &diagnostics)?;
let tool_choice = normalize_tool_choice(&wire.tool_choice, &diagnostics)?;
let reasoning = normalize_reasoning(wire.reasoning.as_ref(), &diagnostics)?;
capabilities.public_reasoning = reasoning == ReasoningIntent::PublicSummary;
let structured_output = normalize_text(wire.text.as_ref(), &diagnostics)?;
capabilities.structured_output = structured_output.is_some();
let request = CanonicalRequest {
wire_version: M1_WIRE_VERSION.to_owned(),
model: wire.model,
stream: true,
harness_instance,
session_id,
turn_id,
input,
tools,
tool_choice,
structured_output,
reasoning,
max_output_tokens: None,
max_spend_micro_usd: None,
capabilities,
};
request
.validate()
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::CanonicalValidation))?;
Ok(request)
}
fn with_input_index(mut error: CodexWireError, input_index: usize) -> CodexWireError {
if let Some(object) = error.diagnostics.as_object_mut() {
object
.entry("failure_stage".to_owned())
.or_insert_with(|| Value::String(FailureStage::InputItemShape.as_str().to_owned()));
object.insert(
"input_index".to_owned(),
Value::from(input_index.min(255) as u64),
);
}
error
}
fn normalize_input_item(
value: &Value,
capabilities: &mut CapabilityRequirements,
diagnostics: &Value,
) -> Result<InputItem, CodexWireError> {
let object = value
.as_object()
.ok_or_else(|| invalid(diagnostics.clone()))?;
let item_type = string_field(object, "type", diagnostics)?;
match item_type {
"message" => normalize_message(object, capabilities, diagnostics),
"agent_message" => normalize_agent_message(object, capabilities, diagnostics),
"function_call" => normalize_function_call(object, diagnostics),
"function_call_output" => normalize_function_output(object, diagnostics),
unsupported @ ("reasoning"
| "apply_patch_call"
| "apply_patch_call_output"
| "code_interpreter_call"
| "computer_call"
| "computer_call_output"
| "file_search_call"
| "item_reference"
| "local_shell_call"
| "local_shell_call_output"
| "custom_tool_call"
| "custom_tool_call_output"
| "tool_search_call"
| "tool_search_output"
| "web_search_call"
| "image_generation_call"
| "compaction"
| "compaction_summary"
| "compaction_trigger"
| "context_compaction"
| "mcp_call"
| "mcp_call_output"
| "mcp_approval_request"
| "mcp_approval_response"
| "mcp_list_tools"
| "mcp_tool_call"
| "mcp_tool_call_output"
| "shell_call"
| "shell_call_output") => Err(wire_error_at_input_item_type(
diagnostics.clone(),
unsupported,
)),
_ => Err(invalid(diagnostics.clone())),
}
}
fn extract_additional_tools(
value: &Value,
diagnostics: &Value,
) -> Result<Vec<Value>, CodexWireError> {
let object = value
.as_object()
.ok_or_else(|| invalid(diagnostics.clone()))?;
ensure_only_keys(object, &["type", "role", "tools"], diagnostics)?;
if string_field(object, "role", diagnostics)? != "developer" {
return Err(invalid(diagnostics.clone()));
}
object
.get("tools")
.and_then(Value::as_array)
.cloned()
.ok_or_else(|| invalid(diagnostics.clone()))
}
fn normalize_agent_message(
object: &Map<String, Value>,
capabilities: &mut CapabilityRequirements,
diagnostics: &Value,
) -> Result<InputItem, CodexWireError> {
if string_field(object, "role", diagnostics)? != "assistant" {
return Err(invalid(diagnostics.clone()));
}
normalize_message(object, capabilities, diagnostics)
}
fn normalize_message(
object: &Map<String, Value>,
capabilities: &mut CapabilityRequirements,
diagnostics: &Value,
) -> Result<InputItem, CodexWireError> {
ensure_only_keys(
object,
&[
"type",
"id",
"role",
"content",
"phase",
"internal_chat_message_metadata_passthrough",
],
diagnostics,
)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputItemShape))?;
let role = match string_field(object, "role", diagnostics)? {
"developer" => MessageRole::Developer,
"system" => MessageRole::System,
"user" => MessageRole::User,
"assistant" => MessageRole::Assistant,
_ => return Err(invalid(diagnostics.clone())),
};
let content = match object.get("content") {
Some(content) => content
.as_array()
.ok_or_else(|| invalid(diagnostics.clone()))?,
None if matches!(role, MessageRole::Developer | MessageRole::System) => {
return Ok(InputItem::Message {
role,
content: Vec::new(),
});
}
None => return Err(invalid(diagnostics.clone())),
};
let mut normalized = Vec::with_capacity(content.len());
for part in content {
let part = part
.as_object()
.ok_or_else(|| invalid(diagnostics.clone()))?;
match string_field(part, "type", diagnostics)? {
"input_text" | "output_text" => {
ensure_only_keys(part, &["type", "text"], diagnostics)?;
normalized.push(ContentBlock::Text {
text: string_field(part, "text", diagnostics)?.to_owned(),
});
}
"input_image" => {
ensure_only_keys(part, &["type", "image_url", "detail"], diagnostics)?;
capabilities.images = true;
normalized.push(ContentBlock::ImageReference {
url: string_field(part, "image_url", diagnostics)?.to_owned(),
});
}
"input_audio" => {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ContentType,
));
}
_ => return Err(invalid(diagnostics.clone())),
}
}
Ok(InputItem::Message {
role,
content: normalized,
})
}
fn normalize_function_call(
object: &Map<String, Value>,
diagnostics: &Value,
) -> Result<InputItem, CodexWireError> {
ensure_only_keys(
object,
&[
"type",
"id",
"name",
"namespace",
"arguments",
"call_id",
"internal_chat_message_metadata_passthrough",
],
diagnostics,
)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputItemShape))?;
let namespace = object
.get("namespace")
.filter(|value| !value.is_null())
.map(|value| {
value
.as_str()
.ok_or_else(|| invalid(diagnostics.clone()))
.and_then(|namespace| {
validate_tool_namespace(namespace).map_err(|_| {
invalid_at(diagnostics.clone(), FailureStage::InputItemNamespace)
})?;
Ok(namespace.to_owned())
})
})
.transpose()?;
let raw_arguments = string_field(object, "arguments", diagnostics)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionArguments))?;
let arguments = serde_json::from_str(raw_arguments)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionArguments))?;
Ok(InputItem::FunctionCall {
call_id: CallId::parse(
string_field(object, "call_id", diagnostics)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionCallId))?,
)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionCallId))?,
namespace,
name: validated_tool_name(object, diagnostics)?,
arguments,
})
}
fn normalize_function_output(
object: &Map<String, Value>,
diagnostics: &Value,
) -> Result<InputItem, CodexWireError> {
ensure_only_keys(
object,
&[
"type",
"id",
"call_id",
"output",
"internal_chat_message_metadata_passthrough",
],
diagnostics,
)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputItemShape))?;
let output = object
.get("output")
.filter(|value| value.is_string() || value.is_array())
.cloned()
.ok_or_else(|| invalid_at(diagnostics.clone(), FailureStage::InputFunctionOutput))?;
Ok(InputItem::FunctionCallOutput {
call_id: CallId::parse(
string_field(object, "call_id", diagnostics)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionCallId))?,
)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::InputFunctionCallId))?,
output,
})
}
fn normalize_tools(
values: Vec<Value>,
diagnostics: &Value,
) -> Result<Vec<FunctionTool>, CodexWireError> {
let mut tools = Vec::new();
let mut namespace_containers = HashSet::new();
for value in values {
let object = value
.as_object()
.ok_or_else(|| invalid(diagnostics.clone()))?;
match string_field(object, "type", diagnostics)? {
"function" => tools.push(normalize_function_tool(object, None, diagnostics)?),
"namespace" => {
ensure_only_keys(
object,
&["type", "name", "description", "tools"],
diagnostics,
)?;
let namespace = string_field(object, "name", diagnostics)?;
validate_tool_namespace(namespace).map_err(|_| {
invalid_at(diagnostics.clone(), FailureStage::ToolNamespaceName)
})?;
if !namespace_containers.insert(namespace.to_owned()) {
return Err(invalid_at(diagnostics.clone(), FailureStage::ToolDuplicate));
}
string_field(object, "description", diagnostics)?;
let inner_tools = object
.get("tools")
.and_then(Value::as_array)
.filter(|tools| !tools.is_empty())
.ok_or_else(|| {
invalid_at(diagnostics.clone(), FailureStage::ToolNamespaceShape)
})?;
for inner in inner_tools {
let inner = inner.as_object().ok_or_else(|| {
invalid_at(diagnostics.clone(), FailureStage::ToolNamespaceShape)
})?;
if string_field(inner, "type", diagnostics)? != "function" {
return Err(invalid_at(
diagnostics.clone(),
FailureStage::ToolNamespaceInnerType,
));
}
tools.push(normalize_function_tool(
inner,
Some(namespace.to_owned()),
diagnostics,
)?);
}
}
"custom" => {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ToolTypeCustom,
));
}
"tool_search" => {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ToolTypeToolSearch,
));
}
"web_search" => {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ToolTypeWebSearch,
));
}
_ => {
return Err(invalid_at(
diagnostics.clone(),
FailureStage::ToolTypeUnknown,
));
}
}
}
validate_flattened_tools(&tools, diagnostics)?;
Ok(tools)
}
fn normalize_function_tool(
object: &Map<String, Value>,
namespace: Option<String>,
diagnostics: &Value,
) -> Result<FunctionTool, CodexWireError> {
ensure_only_keys(object, TOOL_FIELDS, diagnostics)?;
if object
.get("defer_loading")
.and_then(Value::as_bool)
.unwrap_or(false)
{
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ToolDeferLoading,
));
}
Ok(FunctionTool {
namespace,
name: validated_tool_name(object, diagnostics)?,
description: string_field(object, "description", diagnostics)?.to_owned(),
parameters: object
.get("parameters")
.cloned()
.ok_or_else(|| invalid(diagnostics.clone()))?,
strict: object
.get("strict")
.and_then(Value::as_bool)
.ok_or_else(|| invalid(diagnostics.clone()))?,
})
}
fn validated_tool_name(
object: &Map<String, Value>,
diagnostics: &Value,
) -> Result<String, CodexWireError> {
let name = string_field(object, "name", diagnostics)?;
validate_tool_name(name)
.map_err(|_| invalid_at(diagnostics.clone(), FailureStage::ToolName))?;
Ok(name.to_owned())
}
fn validate_flattened_tools(
tools: &[FunctionTool],
diagnostics: &Value,
) -> Result<(), CodexWireError> {
let mut identities = HashSet::new();
let direct_names = tools
.iter()
.filter(|tool| tool.namespace.is_none())
.map(|tool| tool.name.as_str())
.collect::<HashSet<_>>();
for tool in tools {
if !identities.insert((tool.namespace.as_deref(), tool.name.as_str())) {
return Err(invalid_at(diagnostics.clone(), FailureStage::ToolDuplicate));
}
if tool
.namespace
.as_deref()
.is_some_and(|namespace| direct_names.contains(namespace))
{
return Err(invalid_at(
diagnostics.clone(),
FailureStage::ToolReservedCollision,
));
}
}
Ok(())
}
fn normalize_tool_choice(value: &str, diagnostics: &Value) -> Result<ToolChoice, CodexWireError> {
match value {
"auto" => Ok(ToolChoice::Auto),
"none" => Ok(ToolChoice::None),
"required" => Ok(ToolChoice::Required),
_ => Err(wire_error_at(
CodexWireErrorKind::UnsupportedWireCombination,
diagnostics.clone(),
FailureStage::ToolChoice,
)),
}
}
fn normalize_reasoning(
value: Option<&Value>,
diagnostics: &Value,
) -> Result<ReasoningIntent, CodexWireError> {
let Some(value) = value else {
return Ok(ReasoningIntent::None);
};
let object = value
.as_object()
.ok_or_else(|| invalid_at(diagnostics.clone(), FailureStage::Reasoning))?;
ensure_only_keys(object, &["effort", "summary", "context"], diagnostics)?;
validate_optional_enum(
object.get("effort"),
&["none", "minimal", "low", "medium", "high", "xhigh"],
diagnostics,
FailureStage::ReasoningEffort,
)?;
validate_optional_enum(
object.get("context"),
&["auto", "current_turn", "all_turns"],
diagnostics,
FailureStage::ReasoningContext,
)?;
let summary = object.get("summary");
validate_optional_enum(
summary,
&["none", "auto", "concise", "detailed"],
diagnostics,
FailureStage::ReasoningSummary,
)?;
Ok(match summary.and_then(Value::as_str) {
Some("auto" | "concise" | "detailed") => ReasoningIntent::PublicSummary,
Some("none") | None => ReasoningIntent::None,
Some(_) => {
return Err(invalid_at(
diagnostics.clone(),
FailureStage::ReasoningSummary,
));
}
})
}
fn normalize_text(
value: Option<&Value>,
diagnostics: &Value,
) -> Result<Option<StructuredOutputIntent>, CodexWireError> {
let Some(value) = value else {
return Ok(None);
};
let object = value
.as_object()
.ok_or_else(|| invalid_at(diagnostics.clone(), FailureStage::Text))?;
ensure_only_keys(object, &["verbosity", "format"], diagnostics)?;
validate_optional_enum(
object.get("verbosity"),
&["low", "medium", "high"],
diagnostics,
FailureStage::TextVerbosity,
)?;
let Some(format) = object.get("format").filter(|value| !value.is_null()) else {
return Ok(None);
};
let format = format
.as_object()
.ok_or_else(|| invalid_at(diagnostics.clone(), FailureStage::TextFormat))?;
match string_field(format, "type", diagnostics)? {
"text" => {
ensure_only_keys(format, &["type"], diagnostics)?;
return Ok(None);
}
"json_schema" => {
ensure_only_keys(format, &["type", "strict", "schema", "name"], diagnostics)?;
}
_ => {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::TextFormatType,
));
}
}
Ok(Some(StructuredOutputIntent {
name: string_field(format, "name", diagnostics)?.to_owned(),
schema: format
.get("schema")
.cloned()
.ok_or_else(|| invalid(diagnostics.clone()))?,
strict: format
.get("strict")
.and_then(Value::as_bool)
.ok_or_else(|| invalid(diagnostics.clone()))?,
}))
}
fn validate_stream_options(
value: Option<&Value>,
diagnostics: &Value,
) -> Result<(), CodexWireError> {
let Some(value) = value else {
return Ok(());
};
let object = value
.as_object()
.ok_or_else(|| invalid(diagnostics.clone()))?;
ensure_only_keys(object, &["reasoning_summary_delivery"], diagnostics)?;
if string_field(object, "reasoning_summary_delivery", diagnostics)? != "sequential_cutoff" {
return Err(wire_error_at(
CodexWireErrorKind::UnsupportedWireCombination,
diagnostics.clone(),
FailureStage::StreamOptions,
));
}
Ok(())
}
fn validate_include(values: &[String], diagnostics: &Value) -> Result<(), CodexWireError> {
if values
.iter()
.all(|value| value == "reasoning.encrypted_content")
{
Ok(())
} else {
Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::Include,
))
}
}
fn validate_service_tier(value: Option<&str>, diagnostics: &Value) -> Result<(), CodexWireError> {
match value {
None | Some("auto" | "default") => Ok(()),
Some(_) => Err(wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics.clone(),
FailureStage::ServiceTier,
)),
}
}
fn validate_optional_enum(
value: Option<&Value>,
allowed: &[&str],
diagnostics: &Value,
failure_stage: FailureStage,
) -> Result<(), CodexWireError> {
match value {
None | Some(Value::Null) => Ok(()),
Some(Value::String(value)) if allowed.contains(&value.as_str()) => Ok(()),
Some(_) => Err(invalid_at(diagnostics.clone(), failure_stage)),
}
}
fn string_field<'a>(
object: &'a Map<String, Value>,
field: &str,
diagnostics: &Value,
) -> Result<&'a str, CodexWireError> {
object
.get(field)
.and_then(Value::as_str)
.ok_or_else(|| invalid(diagnostics.clone()))
}
fn ensure_only_keys(
object: &Map<String, Value>,
allowed: &[&str],
diagnostics: &Value,
) -> Result<(), CodexWireError> {
if object.keys().all(|key| allowed.contains(&key.as_str())) {
Ok(())
} else {
Err(invalid(diagnostics.clone()))
}
}
fn invalid(diagnostics: Value) -> CodexWireError {
wire_error(CodexWireErrorKind::InvalidRequest, diagnostics)
}
fn invalid_at(diagnostics: Value, failure_stage: FailureStage) -> CodexWireError {
wire_error_at(
CodexWireErrorKind::InvalidRequest,
diagnostics,
failure_stage,
)
}
fn wire_error(kind: CodexWireErrorKind, diagnostics: Value) -> CodexWireError {
CodexWireError { kind, diagnostics }
}
fn wire_error_at(
kind: CodexWireErrorKind,
mut diagnostics: Value,
failure_stage: FailureStage,
) -> CodexWireError {
if let Some(object) = diagnostics.as_object_mut() {
object.insert(
"failure_stage".to_owned(),
Value::String(failure_stage.as_str().to_owned()),
);
}
CodexWireError { kind, diagnostics }
}
fn wire_error_at_input_item_type(mut diagnostics: Value, item_type: &str) -> CodexWireError {
if let Some(object) = diagnostics.as_object_mut() {
object.insert(
"input_item_type".to_owned(),
Value::String(item_type.to_owned()),
);
}
wire_error_at(
CodexWireErrorKind::UnsupportedCapability,
diagnostics,
FailureStage::InputItemType,
)
}
fn structural_diagnostics(value: &Value) -> Value {
let Some(object) = value.as_object() else {
return json!({
"schema": "codex_responses_0_145",
"body_type": json_type(value)
});
};
let top_level_fields = field_types(object, TOP_LEVEL_FIELDS);
let unknown_top_level_field_count = object
.keys()
.filter(|key| !TOP_LEVEL_FIELDS.contains(&key.as_str()))
.count();
let input_item_shapes = array_shapes(object.get("input"), ITEM_FIELDS);
let tool_shapes = array_shapes(object.get("tools"), TOOL_FIELDS);
json!({
"schema": "codex_responses_0_145",
"body_type": "object",
"top_level_fields": top_level_fields,
"unknown_top_level_field_count": unknown_top_level_field_count,
"input_item_shapes": input_item_shapes,
"tool_shapes": tool_shapes
})
}
fn array_shapes(value: Option<&Value>, allowed: &[&str]) -> Vec<Value> {
value
.and_then(Value::as_array)
.into_iter()
.flatten()
.take(8)
.map(|item| match item.as_object() {
Some(object) => Value::Array(field_types(object, allowed)),
None => json!({ "item_type": json_type(item) }),
})
.collect()
}
fn field_types(object: &Map<String, Value>, allowed: &[&str]) -> Vec<Value> {
allowed
.iter()
.filter_map(|field| {
object.get(*field).map(|value| {
json!({
"field": field,
"json_type": json_type(value)
})
})
})
.collect()
}
fn json_type(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn identity() -> CodexRequestIdentity {
CodexRequestIdentity {
session_id: Some("session-header".to_owned()),
thread_id: Some("thread-header".to_owned()),
fallback_id: "request-fallback".to_owned(),
}
}
fn codex_request() -> Value {
json!({
"model": "router-auto",
"instructions": "instruction-canary",
"input": [{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "prompt-canary"}]
}],
"tools": [{
"type": "function",
"name": "get_weather",
"description": "description-canary",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {"effort": "low", "summary": null},
"store": false,
"stream": true,
"include": ["reasoning.encrypted_content"],
"prompt_cache_key": "cache-canary",
"text": {"verbosity": "medium"},
"client_metadata": {
"session_id": "metadata-session",
"thread_id": "metadata-thread",
"turn_id": "metadata-turn",
"x-codex-turn-metadata": "metadata-canary"
}
})
}
#[test]
fn normalizes_real_codex_0_145_shape_without_losing_supported_intent() {
let body = serde_json::to_vec(&codex_request()).expect("serialize fixture");
let request = normalize_responses_request(&body, identity()).expect("normalize fixture");
assert_eq!(request.model, "router-auto");
assert_eq!(request.harness_instance.as_str(), "codex-cli");
assert_eq!(request.session_id.as_str(), "session-header");
assert_eq!(request.turn_id.as_str(), "metadata-turn");
assert_eq!(request.input.len(), 2);
assert_eq!(request.tools.len(), 1);
assert_eq!(request.tools[0].name, "get_weather");
}
#[test]
fn accepts_responses_lite_empty_developer_message_without_widening_user_messages() {
let mut request = codex_request();
request["instructions"] = Value::String(String::new());
request["input"]
.as_array_mut()
.expect("fixture input")
.insert(0, json!({"type": "message", "role": "developer"}));
let body = serde_json::to_vec(&request).expect("serialize fixture");
let normalized =
normalize_responses_request(&body, identity()).expect("empty developer item");
assert!(matches!(
normalized.input.first(),
Some(InputItem::Message {
role: MessageRole::Developer,
content
}) if content.is_empty()
));
request["input"][0] = json!({"type": "message", "role": "user"});
let body = serde_json::to_vec(&request).expect("serialize invalid fixture");
normalize_responses_request(&body, identity())
.expect_err("contentless user message remains invalid");
}
#[test]
fn accepts_current_agent_message_only_as_assistant_history() {
let mut request = codex_request();
request["input"] = json!([{
"type": "agent_message",
"id": "agent-history-id",
"role": "assistant",
"content": [{"type": "output_text", "text": "prior answer"}]
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let normalized =
normalize_responses_request(&body, identity()).expect("agent message history");
assert!(normalized.input.iter().any(|item| matches!(
item,
InputItem::Message {
role: MessageRole::Assistant,
content
} if matches!(content.as_slice(), [ContentBlock::Text { text }] if text == "prior answer")
)));
request["input"][0]["role"] = Value::String("developer".to_owned());
let body = serde_json::to_vec(&request).expect("serialize invalid fixture");
normalize_responses_request(&body, identity())
.expect_err("agent message cannot widen to developer role");
}
#[test]
fn additional_tools_are_developer_scoped_and_join_declared_tools() {
let mut request = codex_request();
let tools = request["tools"].take();
request["tools"] = Value::Null;
request["input"] = json!([{
"type": "additional_tools",
"role": "developer",
"tools": tools
}, {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "prompt"}]
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let normalized =
normalize_responses_request(&body, identity()).expect("additional tool declarations");
assert_eq!(normalized.tools.len(), 1);
assert_eq!(normalized.tools[0].name, "get_weather");
assert!(!normalized.input.iter().any(|item| matches!(
item,
InputItem::Message {
role: MessageRole::Developer,
content
} if content.is_empty()
)));
request["input"][0]["role"] = Value::String("user".to_owned());
let body = serde_json::to_vec(&request).expect("serialize invalid fixture");
normalize_responses_request(&body, identity())
.expect_err("additional tools cannot be user-scoped");
request["input"][0] = json!({
"type": "additional_tools",
"role": "developer",
"tools": []
});
let body = serde_json::to_vec(&request).expect("serialize empty declaration");
let normalized =
normalize_responses_request(&body, identity()).expect("empty declaration is a no-op");
assert!(normalized.tools.is_empty());
}
#[test]
fn diagnostics_contain_only_allowlisted_names_and_json_types() {
let mut request = codex_request();
request["unexpected-secret-field-name"] = Value::String("value-canary".to_owned());
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error =
normalize_responses_request(&body, identity()).expect_err("reject unknown field");
let rendered = serde_json::to_string(&error.diagnostics).expect("render diagnostics");
for canary in [
"instruction-canary",
"prompt-canary",
"description-canary",
"cache-canary",
"metadata-canary",
"value-canary",
"unexpected-secret-field-name",
] {
assert!(!rendered.contains(canary), "diagnostics leaked {canary}");
}
assert!(rendered.contains("top_level_fields"));
assert!(rendered.contains("json_type"));
assert!(rendered.contains("unknown_top_level_field_count"));
}
#[test]
fn unsupported_semantics_fail_closed() {
let mut request = codex_request();
request["tools"][0]["type"] = Value::String("custom".to_owned());
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity()).expect_err("reject custom tool");
assert_eq!(error.kind, CodexWireErrorKind::UnsupportedCapability);
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.type_custom"
);
let rendered = serde_json::to_string(&error.diagnostics).expect("render diagnostics");
for canary in [
"instruction-canary",
"prompt-canary",
"description-canary",
"cache-canary",
"metadata-canary",
] {
assert!(!rendered.contains(canary), "diagnostics leaked {canary}");
}
let mut request = codex_request();
request["store"] = Value::Bool(true);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity()).expect_err("reject store");
assert_eq!(error.kind, CodexWireErrorKind::UnsupportedWireCombination);
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.request.store"
);
}
#[test]
fn failure_stages_are_a_bounded_allowlist() {
let stages = FailureStage::ALL.map(FailureStage::as_str);
assert_eq!(stages.len(), 34);
for (index, stage) in stages.iter().enumerate() {
assert!(stage.starts_with("codex_wire."));
assert!(stage.len() <= 64);
assert!(
stage
.bytes()
.all(|byte| { byte.is_ascii_lowercase() || byte == b'.' || byte == b'_' })
);
assert!(!stages[..index].contains(stage), "duplicate failure stage");
}
}
#[test]
fn tool_type_failures_use_specific_fixed_stages_without_echoing_values() {
for (tool_type, expected_stage) in [
("custom", "codex_wire.tool.type_custom"),
("tool_search", "codex_wire.tool.type_tool_search"),
("web_search", "codex_wire.tool.type_web_search"),
] {
let mut request = codex_request();
request["tools"][0]["type"] = Value::String(tool_type.to_owned());
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity())
.expect_err("known unsupported tool type");
assert_eq!(error.kind, CodexWireErrorKind::UnsupportedCapability);
assert_eq!(error.diagnostics["failure_stage"], expected_stage);
}
let mut request = codex_request();
request["tools"][0]["type"] = Value::String("unknown-type-value-canary".to_owned());
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity()).expect_err("unknown tool type");
assert_eq!(error.kind, CodexWireErrorKind::InvalidRequest);
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.type_unknown"
);
let rendered = serde_json::to_string(&error.diagnostics).expect("render diagnostics");
assert!(!rendered.contains("unknown-type-value-canary"));
}
#[test]
fn unsupported_input_types_emit_only_fixed_discriminators() {
for item_type in [
"reasoning",
"apply_patch_call",
"apply_patch_call_output",
"code_interpreter_call",
"computer_call",
"computer_call_output",
"file_search_call",
"item_reference",
"local_shell_call",
"local_shell_call_output",
"custom_tool_call",
"custom_tool_call_output",
"tool_search_call",
"tool_search_output",
"web_search_call",
"image_generation_call",
"compaction",
"compaction_summary",
"compaction_trigger",
"context_compaction",
"mcp_call",
"mcp_call_output",
"mcp_approval_request",
"mcp_approval_response",
"mcp_list_tools",
"mcp_tool_call",
"mcp_tool_call_output",
"shell_call",
"shell_call_output",
] {
let mut request = codex_request();
request["input"] = json!([{ "type": item_type }]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity())
.expect_err("known unsupported input type");
assert_eq!(error.kind, CodexWireErrorKind::UnsupportedCapability);
assert_eq!(error.diagnostics["input_item_type"], item_type);
assert_eq!(error.diagnostics["input_index"], 0);
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.input_item.type"
);
}
}
#[test]
fn namespace_tools_are_flattened_with_identity_and_round_trip() {
let mut request = codex_request();
request["tools"] = json!([{
"type": "namespace",
"name": "collaboration",
"description": "namespace-description-canary",
"tools": [{
"type": "function",
"name": "spawn_agent",
"description": "inner-description-canary",
"strict": true,
"parameters": {
"type": "object",
"properties": {"task_name": {"type": "string"}},
"required": ["task_name"],
"additionalProperties": false
}
}]
}]);
let body = serde_json::to_vec(&request).expect("serialize namespace fixture");
let request = normalize_responses_request(&body, identity()).expect("namespace tool");
assert_eq!(request.tools.len(), 1);
assert_eq!(request.tools[0].namespace.as_deref(), Some("collaboration"));
assert_eq!(request.tools[0].name, "spawn_agent");
assert_eq!(
request.tools[0].parameters,
json!({
"type": "object",
"properties": {"task_name": {"type": "string"}},
"required": ["task_name"],
"additionalProperties": false
})
);
let round_trip = serde_json::to_value(&request).expect("canonical serialization");
assert_eq!(round_trip["tools"][0]["namespace"], "collaboration");
assert_eq!(
round_trip["tools"][0]["parameters"],
request.tools[0].parameters
);
}
#[test]
fn namespaced_function_call_history_preserves_namespace_and_arguments() {
let mut request = codex_request();
request["input"] = json!([{
"type": "function_call",
"call_id": "call-namespace-history",
"namespace": "collaboration",
"name": "spawn_agent",
"arguments": "{\"task\":\"fixture\"}"
}]);
let body = serde_json::to_vec(&request).expect("serialize function call history");
let request =
normalize_responses_request(&body, identity()).expect("function call history");
assert!(request.input.iter().any(|item| matches!(
item,
InputItem::FunctionCall {
namespace: Some(namespace),
name,
arguments,
..
} if namespace == "collaboration"
&& name == "spawn_agent"
&& arguments == &json!({"task": "fixture"})
)));
}
#[test]
fn namespace_tools_reject_duplicates_collisions_and_deferred_semantics() {
let namespace = json!({
"type": "namespace",
"name": "collaboration",
"description": "namespace description",
"tools": [{
"type": "function",
"name": "spawn_agent",
"description": "inner description",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}]
});
let mut request = codex_request();
request["tools"] = json!([namespace.clone(), namespace.clone()]);
let body = serde_json::to_vec(&request).expect("serialize duplicate namespace");
let error =
normalize_responses_request(&body, identity()).expect_err("duplicate namespace");
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.duplicate"
);
let mut request = codex_request();
request["tools"] = json!([{
"type": "function",
"name": "collaboration",
"description": "direct collision",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}, namespace.clone()]);
let body = serde_json::to_vec(&request).expect("serialize collision");
let error = normalize_responses_request(&body, identity()).expect_err("reserved collision");
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.reserved_collision"
);
let mut deferred = namespace;
deferred["tools"][0]["defer_loading"] = Value::Bool(true);
let mut request = codex_request();
request["tools"] = json!([deferred]);
let body = serde_json::to_vec(&request).expect("serialize deferred tool");
let error = normalize_responses_request(&body, identity()).expect_err("deferred namespace");
assert_eq!(error.kind, CodexWireErrorKind::UnsupportedCapability);
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.defer_loading"
);
}
#[test]
fn namespace_and_inner_names_are_bounded_identifiers() {
let mut request = codex_request();
request["tools"] = json!([{
"type": "namespace",
"name": "namespace",
"description": "reserved namespace",
"tools": [{
"type": "function",
"name": "tool",
"description": "description",
"strict": true,
"parameters": {}
}]
}]);
let body = serde_json::to_vec(&request).expect("serialize reserved namespace");
let error = normalize_responses_request(&body, identity()).expect_err("reserved namespace");
assert_eq!(
error.diagnostics["failure_stage"],
"codex_wire.tool.namespace.name"
);
request["tools"][0]["name"] = Value::String("valid_namespace".to_owned());
request["tools"][0]["tools"][0]["name"] = Value::String("x".repeat(65));
let body = serde_json::to_vec(&request).expect("serialize long tool name");
let error = normalize_responses_request(&body, identity()).expect_err("long tool name");
assert_eq!(error.diagnostics["failure_stage"], "codex_wire.tool.name");
}
#[test]
fn accepts_exact_plain_text_format_without_creating_structured_output_intent() {
let mut request = codex_request();
request["text"] = json!({
"format": {"type": "text"}
});
let body = serde_json::to_vec(&request).expect("serialize fixture");
let request = normalize_responses_request(&body, identity()).expect("plain text format");
assert_eq!(request.structured_output, None);
assert!(!request.capabilities.structured_output);
let mut request = codex_request();
request["text"] = json!({
"format": {"type": "text", "schema": {}}
});
let body = serde_json::to_vec(&request).expect("serialize fixture");
let error = normalize_responses_request(&body, identity())
.expect_err("plain text format must reject extra semantics");
assert_eq!(error.kind, CodexWireErrorKind::InvalidRequest);
}
#[test]
fn provider_tool_call_normalization_round_trips_flattened_namespaces() {
assert_eq!(
normalize_provider_tool_call("multi_agent_v1__spawn_agent", &json!({})),
(Some("multi_agent_v1".to_owned()), "spawn_agent".to_owned())
);
assert_eq!(
normalize_provider_tool_call("multi_agent_v1__send_input", &json!({})),
(Some("multi_agent_v1".to_owned()), "send_input".to_owned())
);
assert_eq!(
normalize_provider_tool_call("weather__lookup", &json!({})),
(Some("weather".to_owned()), "lookup".to_owned())
);
}
#[test]
fn canonical_multi_agent_context_is_detected_with_fixed_owner_limits() {
let mut request = codex_request();
request["tools"] = json!([{
"type": "namespace",
"name": "multi_agent_v1",
"description": "canonical multi-agent namespace",
"tools": [{
"type": "function",
"name": "spawn_agent",
"description": "spawn an agent",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}]
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let canonical =
normalize_responses_request(&body, identity()).expect("normalize canonical context");
assert_eq!(
canonical_subagent_request_context(&canonical),
Some(CodexSubagentRequestContext {
max_threads: 4,
max_depth: 2,
fork_turns: "none",
})
);
let ordinary = normalize_responses_request(
&serde_json::to_vec(&codex_request()).expect("serialize ordinary fixture"),
identity(),
)
.expect("normalize ordinary fixture");
assert_eq!(canonical_subagent_request_context(&ordinary), None);
}
#[test]
fn provider_tool_call_normalization_disambiguates_bare_multi_agent_by_argument_shape() {
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"task_name": "t", "message": "do it"})
),
(Some("multi_agent_v1".to_owned()), "spawn_agent".to_owned())
);
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"target": "agent-1", "message": "continue"})
),
(Some("multi_agent_v1".to_owned()), "send_input".to_owned())
);
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"target": "agent-1", "items": []})
),
(Some("multi_agent_v1".to_owned()), "send_input".to_owned())
);
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"target": "agent-1", "interrupt": true})
),
(Some("multi_agent_v1".to_owned()), "send_input".to_owned())
);
assert_eq!(
normalize_provider_tool_call("multi_agent_v1", &json!({"id": "agent-1"})),
(Some("multi_agent_v1".to_owned()), "resume_agent".to_owned())
);
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"target": "agent-1", "timeout_ms": 30_000})
),
(Some("multi_agent_v1".to_owned()), "wait_agent".to_owned())
);
assert_eq!(
normalize_provider_tool_call("multi_agent_v1", &json!({"target": "agent-1"})),
(Some("multi_agent_v1".to_owned()), "close_agent".to_owned())
);
assert_eq!(
normalize_provider_tool_call("multi_agent_v1", &json!({})),
(Some("multi_agent_v1".to_owned()), "spawn_agent".to_owned())
);
}
#[test]
fn provider_tool_call_normalization_leaves_plain_and_invalid_names_unchanged() {
assert_eq!(
normalize_provider_tool_call("get_weather", &json!({})),
(None, "get_weather".to_owned())
);
assert_eq!(
normalize_provider_tool_call("function__lookup", &json!({})),
(None, "function__lookup".to_owned())
);
assert_eq!(
normalize_provider_tool_call("multi_agent_v1__", &json!({})),
(None, "multi_agent_v1__".to_owned())
);
}
#[test]
fn four_route_request_side_translation_round_trips_through_normalization() {
let flatten = |namespace: Option<&str>, name: &str| {
namespace.map_or_else(
|| name.to_owned(),
|namespace| format!("{namespace}__{name}"),
)
};
assert_eq!(
normalize_provider_tool_call("spawn_agent", &json!({})),
(None, "spawn_agent".to_owned())
);
for route in ["ollama_cloud", "openrouter", "ollama_local"] {
let request_name = flatten(Some("multi_agent_v1"), "spawn_agent");
assert_eq!(request_name, "multi_agent_v1__spawn_agent");
assert_eq!(
normalize_provider_tool_call(&request_name, &json!({})),
(Some("multi_agent_v1".to_owned()), "spawn_agent".to_owned())
);
let _ = route;
}
assert_eq!(
normalize_provider_tool_call(
"multi_agent_v1",
&json!({"target": "agent-1", "message": "continue"})
),
(Some("multi_agent_v1".to_owned()), "send_input".to_owned())
);
}
#[test]
fn mav2_provider_tool_call_normalization_maps_bare_tools_to_chosen_namespace() {
use router_protocol::MultiAgentNamespace;
for namespace in [
MultiAgentNamespace::Collaboration,
MultiAgentNamespace::Agents,
] {
for tool in MAV2_TOOL_NAMES {
assert_eq!(
normalize_mav2_provider_tool_call(tool, &json!({}), namespace),
(Some(namespace.as_str().to_owned()), tool.to_owned())
);
}
}
assert_eq!(
normalize_mav2_provider_tool_call(
"spawn_agent",
&json!({"task_name": "t", "message": "Reply OK"}),
MultiAgentNamespace::Agents
),
(Some("agents".to_owned()), "spawn_agent".to_owned())
);
assert_eq!(
normalize_mav2_provider_tool_call(
"spawn_agent",
&json!({"task_name": "t"}),
MultiAgentNamespace::Collaboration
),
(Some("collaboration".to_owned()), "spawn_agent".to_owned())
);
for (namespace, expected) in [("collaboration", "collaboration"), ("agents", "agents")] {
assert_eq!(
normalize_mav2_provider_tool_call(
&format!("{namespace}__send_input"),
&json!({"target": "a1", "message": "continue"}),
MultiAgentNamespace::Agents
),
(Some(expected.to_owned()), "send_input".to_owned())
);
}
assert_eq!(
normalize_mav2_provider_tool_call(
"get_weather",
&json!({"city": "Paris"}),
MultiAgentNamespace::Collaboration
),
(None, "get_weather".to_owned())
);
assert_eq!(
normalize_mav2_provider_tool_call(
"multi_agent_v1",
&json!({"id": "agent-1"}),
MultiAgentNamespace::Agents
),
(Some("multi_agent_v1".to_owned()), "resume_agent".to_owned())
);
}
#[test]
fn mav2_canonical_subagent_context_is_detected_for_collaboration_agents_and_bare_tools() {
let mut request = codex_request();
request["tools"] = json!([{
"type": "namespace",
"name": "collaboration",
"description": "canonical MAv2 namespace",
"tools": [{
"type": "function",
"name": "spawn_agent",
"description": "spawn an agent",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}]
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let canonical =
normalize_responses_request(&body, identity()).expect("normalize canonical context");
assert_eq!(
canonical_subagent_request_context(&canonical),
Some(CodexSubagentRequestContext {
max_threads: 4,
max_depth: 2,
fork_turns: "none",
})
);
let mut request = codex_request();
request["tools"] = json!([{
"type": "namespace",
"name": "agents",
"description": "non-reserved MAv2 namespace",
"tools": [{
"type": "function",
"name": "spawn_agent",
"description": "spawn an agent",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}]
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let canonical =
normalize_responses_request(&body, identity()).expect("normalize agents context");
assert!(canonical_subagent_request_context(&canonical).is_some());
let mut request = codex_request();
request["tools"] = json!([{
"type": "function",
"name": "spawn_agent",
"description": "bare MAv2 tool declaration",
"strict": true,
"parameters": {"type": "object", "properties": {}}
}]);
let body = serde_json::to_vec(&request).expect("serialize fixture");
let canonical =
normalize_responses_request(&body, identity()).expect("normalize bare MAv2 tool");
assert!(canonical_subagent_request_context(&canonical).is_some());
}
#[test]
fn mav2_spawn_agent_arguments_validate_fork_turns_and_reject_fork_context() {
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"task_name": "t"})),
Ok(Mav2SpawnAgentContract {
task_name: "t".to_owned(),
model: None,
reasoning_effort: None,
fork_turns: Mav2ForkTurns::None,
})
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({
"task_name": "t",
"model": "gpt-5.6-terra",
"reasoning_effort": "high",
"fork_turns": "all"
})),
Ok(Mav2SpawnAgentContract {
task_name: "t".to_owned(),
model: Some("gpt-5.6-terra".to_owned()),
reasoning_effort: Some("high".to_owned()),
fork_turns: Mav2ForkTurns::All,
})
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"fork_turns": 3})),
Ok(Mav2SpawnAgentContract {
task_name: String::new(),
model: None,
reasoning_effort: None,
fork_turns: Mav2ForkTurns::Turns(3),
})
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"fork_context": "parent"})),
Err(Mav2SpawnAgentError::ForkContextRejected)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"fork_turns": 0})),
Err(Mav2SpawnAgentError::InvalidForkTurns)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"fork_turns": "sometimes"})),
Err(Mav2SpawnAgentError::InvalidForkTurns)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"model": "not a model"})),
Err(Mav2SpawnAgentError::InvalidModelOverride)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!({"reasoning_effort": "turbo"})),
Err(Mav2SpawnAgentError::InvalidReasoningEffortOverride)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!("spawn a subagent")),
Err(Mav2SpawnAgentError::InvalidArguments)
);
assert_eq!(
validate_mav2_spawn_agent_arguments(&json!(["fork_turns", "all"])),
Err(Mav2SpawnAgentError::InvalidArguments)
);
}
#[test]
fn namespace_call_id_encode_decode_is_idempotent_and_collision_proof() {
use router_protocol::MultiAgentNamespace;
for namespace in [
MultiAgentNamespace::Collaboration,
MultiAgentNamespace::Agents,
] {
let encoded = encode_namespace_call_id("call-1234", namespace.as_str())
.expect("encode namespace call id");
assert!(encoded.starts_with("call-1234"));
assert!(encoded.contains(NAMESPACE_ID_SMUGGLE_MARKER));
assert_eq!(
decode_namespace_call_id(&encoded),
Some(("call-1234".to_owned(), namespace.as_str().to_owned()))
);
assert_eq!(
encode_namespace_call_id(&encoded, namespace.as_str()).expect("idempotent encode"),
encoded
);
}
let once = encode_namespace_call_id("call-1", "collaboration").expect("encode once");
let twice = encode_namespace_call_id(&once, "agents").expect("encode twice");
assert_eq!(once, twice);
assert_eq!(once.matches(NAMESPACE_ID_SMUGGLE_MARKER).count(), 1);
assert_eq!(
encode_namespace_call_id("call__ns__already", "collaboration"),
Err(NamespaceIdSmuggleError::MarkerCollision)
);
assert_eq!(decode_namespace_call_id("call-1234"), None);
assert_eq!(decode_namespace_call_id("call__ns__b2RkLXRvb2w"), None);
}
}