use std::collections::HashMap;
use axum::http::HeaderMap;
use nemo_relay::api::llm::LlmRequest;
use serde_json::{Map, Value, json};
use crate::configuration::header_string;
pub(crate) use crate::events::json_path::{
string_at_any as json_string_at, value_at_any as json_value_at,
};
use crate::events::{AgentKind, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent};
#[path = "../claude/alignment.rs"]
pub(crate) mod claude_code;
#[path = "../codex/alignment.rs"]
pub(crate) mod codex;
const REQUEST_AFFINITY_KEY_MIN_CHARS: usize = 24;
const REQUEST_AFFINITY_KEY_MAX_CHARS: usize = 4096;
#[derive(Debug, Clone)]
pub(crate) enum SubagentSessionContext {
Codex(codex::SubagentContext),
}
impl SubagentSessionContext {
pub(crate) fn parent_session_id(&self) -> &str {
match self {
Self::Codex(context) => &context.parent_session_id,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum GatewayRouteKind {
OpenAiResponses,
OpenAiChatCompletions,
OpenAiModels,
AnthropicMessages,
AnthropicCountTokens,
}
impl GatewayRouteKind {
pub(crate) const ALL: [Self; 5] = [
Self::OpenAiResponses,
Self::OpenAiChatCompletions,
Self::OpenAiModels,
Self::AnthropicMessages,
Self::AnthropicCountTokens,
];
pub(crate) const fn name(self) -> &'static str {
match self {
Self::OpenAiResponses => "openai.responses",
Self::OpenAiChatCompletions => "openai.chat_completions",
Self::OpenAiModels => "openai.models",
Self::AnthropicMessages => "anthropic.messages",
Self::AnthropicCountTokens => "anthropic.count_tokens",
}
}
pub(crate) fn from_provider_name(provider: &str) -> Option<Self> {
Self::ALL.into_iter().find(|route| route.name() == provider)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum GatewayManagementPolicy {
Managed,
UnmanagedProbe {
status: &'static str,
source: &'static str,
},
}
impl GatewayManagementPolicy {
pub(crate) fn bypasses_managed_pipeline(self) -> bool {
matches!(self, Self::UnmanagedProbe { .. })
}
pub(crate) fn bypass_correlation(self) -> Option<(&'static str, &'static str)> {
match self {
Self::Managed => None,
Self::UnmanagedProbe { status, source } => Some((status, source)),
}
}
}
pub(crate) trait ProviderRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, body: &Value) -> Option<String>;
fn request_affinity_key(&self, request: &LlmRequest) -> Option<String>;
fn gateway_turn_input(&self, agent_kind: AgentKind, request: &LlmRequest) -> Option<Value>;
}
struct OpenAiResponsesRequestExtractor;
struct OpenAiChatCompletionsRequestExtractor;
struct OpenAiModelsRequestExtractor;
struct AnthropicMessagesRequestExtractor;
struct AnthropicCountTokensRequestExtractor;
static OPENAI_RESPONSES_REQUEST_EXTRACTOR: OpenAiResponsesRequestExtractor =
OpenAiResponsesRequestExtractor;
static OPENAI_CHAT_COMPLETIONS_REQUEST_EXTRACTOR: OpenAiChatCompletionsRequestExtractor =
OpenAiChatCompletionsRequestExtractor;
static OPENAI_MODELS_REQUEST_EXTRACTOR: OpenAiModelsRequestExtractor = OpenAiModelsRequestExtractor;
static ANTHROPIC_MESSAGES_REQUEST_EXTRACTOR: AnthropicMessagesRequestExtractor =
AnthropicMessagesRequestExtractor;
static ANTHROPIC_COUNT_TOKENS_REQUEST_EXTRACTOR: AnthropicCountTokensRequestExtractor =
AnthropicCountTokensRequestExtractor;
impl ProviderRequestExtractor for OpenAiResponsesRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, body: &Value) -> Option<String> {
gateway_header_session_id(headers)
.or_else(|| codex::responses_session_id(body, GatewayRouteKind::OpenAiResponses))
.or_else(|| openai_body_session_id(body, GatewayRouteKind::OpenAiResponses))
}
fn request_affinity_key(&self, request: &LlmRequest) -> Option<String> {
affinity_key_from_task_text(responses_user_task_text(&request.content)?)
}
fn gateway_turn_input(&self, _agent_kind: AgentKind, _request: &LlmRequest) -> Option<Value> {
None
}
}
impl ProviderRequestExtractor for OpenAiChatCompletionsRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, body: &Value) -> Option<String> {
gateway_header_session_id(headers)
.or_else(|| openai_body_session_id(body, GatewayRouteKind::OpenAiChatCompletions))
}
fn request_affinity_key(&self, request: &LlmRequest) -> Option<String> {
affinity_key_from_task_text(messages_user_task_text(&request.content)?)
}
fn gateway_turn_input(&self, _agent_kind: AgentKind, _request: &LlmRequest) -> Option<Value> {
None
}
}
impl ProviderRequestExtractor for OpenAiModelsRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, _body: &Value) -> Option<String> {
gateway_header_session_id(headers)
}
fn request_affinity_key(&self, _request: &LlmRequest) -> Option<String> {
None
}
fn gateway_turn_input(&self, _agent_kind: AgentKind, _request: &LlmRequest) -> Option<Value> {
None
}
}
impl ProviderRequestExtractor for AnthropicMessagesRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, _body: &Value) -> Option<String> {
gateway_header_session_id(headers)
}
fn request_affinity_key(&self, request: &LlmRequest) -> Option<String> {
affinity_key_from_task_text(messages_user_task_text(&request.content)?)
}
fn gateway_turn_input(&self, agent_kind: AgentKind, request: &LlmRequest) -> Option<Value> {
if agent_kind != AgentKind::ClaudeCode {
return None;
}
messages_user_task_text(&request.content).map(|prompt| json!({ "prompt": prompt }))
}
}
impl ProviderRequestExtractor for AnthropicCountTokensRequestExtractor {
fn gateway_session_id(&self, headers: &HeaderMap, _body: &Value) -> Option<String> {
gateway_header_session_id(headers)
}
fn request_affinity_key(&self, _request: &LlmRequest) -> Option<String> {
None
}
fn gateway_turn_input(&self, _agent_kind: AgentKind, _request: &LlmRequest) -> Option<Value> {
None
}
}
#[derive(Debug, Clone)]
pub(crate) struct SessionAlias {
pub(crate) parent_session_id: String,
pub(crate) subagent_id: String,
metadata: Value,
}
impl SessionAlias {
pub(crate) fn new(parent_session_id: String, subagent_id: String, metadata: Value) -> Self {
Self {
parent_session_id,
subagent_id,
metadata,
}
}
pub(crate) fn metadata(&self) -> Value {
self.metadata.clone()
}
}
#[derive(Debug, Clone)]
pub(crate) struct PendingSubagentStart {
pub(crate) event: SessionEvent,
context: SubagentSessionContext,
}
impl PendingSubagentStart {
pub(crate) fn parent_session_id(&self) -> &str {
self.context.parent_session_id()
}
pub(crate) fn subagent_start_event(&self) -> SubagentEvent {
subagent_start_event(&self.event, &self.context)
}
pub(crate) fn alias_for_child_session(&self, child_session_id: String) -> SessionAlias {
alias_for_child_session(child_session_id, &self.context)
}
}
#[derive(Debug, Default)]
pub(crate) struct SessionAlignmentState {
aliases: HashMap<String, SessionAlias>,
pending_subagents: HashMap<String, PendingSubagentStart>,
}
impl SessionAlignmentState {
pub(crate) fn clear(&mut self) {
self.aliases.clear();
self.pending_subagents.clear();
}
pub(crate) fn alias_for_session(&self, session_id: &str) -> Option<SessionAlias> {
self.aliases.get(session_id).cloned()
}
#[cfg(test)]
pub(crate) fn has_alias(&self, session_id: &str) -> bool {
self.aliases.contains_key(session_id)
}
#[cfg(test)]
pub(crate) fn has_pending_session(&self, session_id: &str) -> bool {
self.pending_subagents.contains_key(session_id)
}
pub(crate) fn pending_for_session(&mut self, session_id: &str) -> Option<PendingSubagentStart> {
self.pending_subagents.remove(session_id)
}
pub(crate) fn insert_pending(
&mut self,
child_session_id: String,
pending: PendingSubagentStart,
) {
self.pending_subagents.insert(child_session_id, pending);
}
pub(crate) fn remove_pending(&mut self, child_session_id: &str) {
self.pending_subagents.remove(child_session_id);
}
pub(crate) fn insert_alias(&mut self, child_session_id: String, alias: SessionAlias) {
self.aliases.insert(child_session_id, alias);
}
pub(crate) fn route_event(&mut self, event: NormalizedEvent) -> NormalizedEvent {
let (event, finished_alias) = route_event_through_alias(event, &self.aliases);
let session_id = event.session_id().to_string();
if let Some(child_session_id) = finished_alias.as_ref() {
self.aliases.remove(child_session_id);
self.pending_subagents.remove(child_session_id);
}
if matches!(&event, NormalizedEvent::AgentEnded(_)) {
self.clear_for_ended_agent(&session_id);
}
event
}
pub(crate) fn pending_for_parent(
&mut self,
parent_session_id: &str,
) -> Vec<(String, PendingSubagentStart)> {
let child_session_ids = self
.pending_subagents
.iter()
.filter_map(|(child_session_id, pending)| {
(pending.parent_session_id() == parent_session_id)
.then_some(child_session_id.clone())
})
.collect::<Vec<_>>();
child_session_ids
.into_iter()
.filter_map(|child_session_id| {
self.pending_subagents
.remove(&child_session_id)
.map(|pending| (child_session_id, pending))
})
.collect()
}
pub(crate) fn clear_for_ended_agent(&mut self, session_id: &str) {
self.aliases.retain(|child_session_id, alias| {
child_session_id != session_id && alias.parent_session_id != session_id
});
self.pending_subagents.retain(|child_session_id, pending| {
child_session_id != session_id && pending.parent_session_id() != session_id
});
}
pub(crate) fn clear_for_ended_subagent(&mut self, parent_session_id: &str, subagent_id: &str) {
self.aliases.retain(|child_session_id, alias| {
child_session_id != subagent_id
&& !(alias.parent_session_id == parent_session_id
&& alias.subagent_id == subagent_id)
});
self.pending_subagents.retain(|child_session_id, pending| {
child_session_id != subagent_id
&& !(pending.parent_session_id() == parent_session_id
&& pending.event.session_id == subagent_id)
});
}
}
pub(crate) fn gateway_session_id(
headers: &HeaderMap,
body: &Value,
route: GatewayRouteKind,
) -> Option<String> {
provider_request_extractor(route).gateway_session_id(headers, body)
}
fn provider_request_extractor(route: GatewayRouteKind) -> &'static dyn ProviderRequestExtractor {
match route {
GatewayRouteKind::OpenAiResponses => &OPENAI_RESPONSES_REQUEST_EXTRACTOR,
GatewayRouteKind::OpenAiChatCompletions => &OPENAI_CHAT_COMPLETIONS_REQUEST_EXTRACTOR,
GatewayRouteKind::OpenAiModels => &OPENAI_MODELS_REQUEST_EXTRACTOR,
GatewayRouteKind::AnthropicMessages => &ANTHROPIC_MESSAGES_REQUEST_EXTRACTOR,
GatewayRouteKind::AnthropicCountTokens => &ANTHROPIC_COUNT_TOKENS_REQUEST_EXTRACTOR,
}
}
fn provider_request_extractor_for_name(
provider: &str,
) -> Option<&'static dyn ProviderRequestExtractor> {
GatewayRouteKind::from_provider_name(provider).map(provider_request_extractor)
}
fn gateway_header_session_id(headers: &HeaderMap) -> Option<String> {
header_string(headers, "x-nemo-relay-session-id")
.or_else(|| claude_code::session_id_from_headers(headers))
}
fn openai_body_session_id(body: &Value, route: GatewayRouteKind) -> Option<String> {
if !matches!(
route,
GatewayRouteKind::OpenAiChatCompletions | GatewayRouteKind::OpenAiResponses
) {
return None;
}
body.get("session_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|session_id| !session_id.is_empty())
.map(ToOwned::to_owned)
}
pub(crate) fn gateway_upstream_url_override(
headers: &HeaderMap,
route: GatewayRouteKind,
path_and_query: &str,
has_openai_replacement_key: bool,
) -> Option<String> {
codex::chatgpt_upstream_url_if_needed(
headers,
route,
path_and_query,
has_openai_replacement_key,
)
}
pub(crate) fn gateway_forward_headers(
headers: &HeaderMap,
route: GatewayRouteKind,
has_openai_replacement_key: bool,
) -> HeaderMap {
codex::strip_chatgpt_auth_for_openai_route(headers, route, has_openai_replacement_key)
}
pub(crate) fn gateway_subagent_id(
headers: &HeaderMap,
body: &Value,
route: GatewayRouteKind,
) -> Option<String> {
header_string(headers, "x-nemo-relay-subagent-id")
.or_else(|| codex::responses_subagent_id(body, route))
}
pub(crate) fn gateway_identifier(
headers: &HeaderMap,
body: &Value,
header_name: &'static str,
body_paths: &[&[&str]],
) -> Option<String> {
header_string(headers, header_name).or_else(|| json_string_at(body, body_paths))
}
pub(crate) fn agent_kind_for_gateway_provider(provider: &str) -> AgentKind {
if claude_code::owns_gateway_provider(provider) {
AgentKind::ClaudeCode
} else if codex::owns_gateway_provider(provider) {
AgentKind::Codex
} else {
AgentKind::Gateway
}
}
pub(crate) fn gateway_management_policy(
agent_kind: AgentKind,
provider: &str,
model_name: Option<&str>,
request: &LlmRequest,
) -> GatewayManagementPolicy {
if agent_kind == AgentKind::ClaudeCode
&& claude_code::is_startup_probe(provider, model_name, request)
{
GatewayManagementPolicy::UnmanagedProbe {
status: "pre_turn_probe",
source: "claude_startup_probe",
}
} else {
GatewayManagementPolicy::Managed
}
}
pub(crate) fn should_emit_session_agent_scope(agent_kind: AgentKind) -> bool {
!matches!(agent_kind, AgentKind::ClaudeCode | AgentKind::Codex)
}
pub(crate) async fn subagent_session_context(
event: &SessionEvent,
) -> Option<SubagentSessionContext> {
codex::subagent_context(event)
.await
.map(SubagentSessionContext::Codex)
}
pub(crate) async fn pending_subagent_start(
event: &mut NormalizedEvent,
) -> Option<(String, PendingSubagentStart)> {
let NormalizedEvent::AgentStarted(session_event) = event else {
return None;
};
let context = subagent_session_context(session_event).await?;
let child_session_id = session_event.session_id.clone();
if context.parent_session_id() == child_session_id {
return None;
}
session_event.metadata =
augment_subagent_session_metadata(session_event.metadata.clone(), &context);
Some((
child_session_id,
PendingSubagentStart {
event: session_event.clone(),
context,
},
))
}
pub(crate) fn augment_subagent_session_metadata(
metadata: Value,
context: &SubagentSessionContext,
) -> Value {
match context {
SubagentSessionContext::Codex(context) => {
codex::augment_subagent_metadata(metadata, context)
}
}
}
pub(crate) fn subagent_start_event(
event: &SessionEvent,
context: &SubagentSessionContext,
) -> SubagentEvent {
match context {
SubagentSessionContext::Codex(context) => codex::subagent_start_event(event, context),
}
}
pub(crate) fn alias_for_child_session(
child_session_id: String,
context: &SubagentSessionContext,
) -> SessionAlias {
match context {
SubagentSessionContext::Codex(context) => {
codex::alias_for_child_session(child_session_id, context)
}
}
}
pub(crate) fn llm_owner_metadata(scope_metadata: Option<&Value>) -> Value {
codex::llm_owner_metadata(scope_metadata)
}
pub(crate) fn request_affinity_key(provider: &str, request: &LlmRequest) -> Option<String> {
provider_request_extractor_for_name(provider)?.request_affinity_key(request)
}
pub(crate) fn gateway_turn_input(
agent_kind: AgentKind,
provider: &str,
request: &LlmRequest,
) -> Option<Value> {
provider_request_extractor_for_name(provider)?.gateway_turn_input(agent_kind, request)
}
pub(crate) fn completed_subagent_from_tool(event: &ToolEvent) -> Option<String> {
claude_code::completed_subagent_from_agent_tool(event)
}
pub(crate) fn aliased_turn_subagent_id(event: &SessionEvent) -> Option<String> {
json_string_at(
&event.metadata,
&[
&["subagent_id"][..],
&["codex_subagent_session_id"][..],
&["subagent_session_id"][..],
],
)
}
pub(crate) fn route_event_through_alias(
event: NormalizedEvent,
aliases: &HashMap<String, SessionAlias>,
) -> (NormalizedEvent, Option<String>) {
let child_session_id = event.session_id().to_string();
let Some(alias) = aliases.get(&child_session_id).cloned() else {
return (event, None);
};
let metadata = alias.metadata();
match event {
NormalizedEvent::AgentStarted(event) => (
NormalizedEvent::SubagentStarted(SubagentEvent {
session_id: alias.parent_session_id,
agent_kind: event.agent_kind,
event_name: event.event_name,
subagent_id: alias.subagent_id,
payload: event.payload,
metadata: merge_metadata(event.metadata, metadata),
}),
None,
),
NormalizedEvent::AgentEnded(event) => (
NormalizedEvent::SubagentEnded(SubagentEvent {
session_id: alias.parent_session_id,
agent_kind: event.agent_kind,
event_name: event.event_name,
subagent_id: alias.subagent_id,
payload: event.payload,
metadata: merge_metadata(event.metadata, metadata),
}),
Some(child_session_id),
),
NormalizedEvent::TurnEnded(mut event) => {
route_session_event(&mut event, &alias, metadata);
(NormalizedEvent::TurnEnded(event), Some(child_session_id))
}
NormalizedEvent::PromptSubmitted(mut event) => {
route_session_event(&mut event, &alias, metadata);
(NormalizedEvent::PromptSubmitted(event), None)
}
NormalizedEvent::Compaction(mut event) => {
route_session_event(&mut event, &alias, metadata);
(NormalizedEvent::Compaction(event), None)
}
NormalizedEvent::Notification(mut event) => {
route_session_event(&mut event, &alias, metadata);
(NormalizedEvent::Notification(event), None)
}
NormalizedEvent::HookMark(mut event) => {
route_session_event(&mut event, &alias, metadata);
(NormalizedEvent::HookMark(event), None)
}
NormalizedEvent::SubagentStarted(mut event) => {
route_subagent_event(&mut event, &alias, metadata);
(NormalizedEvent::SubagentStarted(event), None)
}
NormalizedEvent::SubagentEnded(mut event) => {
route_subagent_event(&mut event, &alias, metadata);
(NormalizedEvent::SubagentEnded(event), None)
}
NormalizedEvent::LlmHint(mut event) => {
event.session_id = alias.parent_session_id;
event.subagent_id = Some(alias.subagent_id);
event.metadata = merge_metadata(event.metadata, metadata);
(NormalizedEvent::LlmHint(event), None)
}
NormalizedEvent::ToolStarted(mut event) => {
route_tool_event(&mut event, &alias, metadata);
(NormalizedEvent::ToolStarted(event), None)
}
NormalizedEvent::ToolEnded(mut event) => {
route_tool_event(&mut event, &alias, metadata);
(NormalizedEvent::ToolEnded(event), None)
}
}
}
fn route_session_event(event: &mut SessionEvent, alias: &SessionAlias, metadata: Value) {
event.session_id = alias.parent_session_id.clone();
event.metadata = merge_metadata(event.metadata.clone(), metadata);
}
fn route_subagent_event(event: &mut SubagentEvent, alias: &SessionAlias, metadata: Value) {
event.session_id = alias.parent_session_id.clone();
event.metadata = merge_metadata(event.metadata.clone(), metadata);
}
fn route_tool_event(event: &mut ToolEvent, alias: &SessionAlias, metadata: Value) {
event.session_id = alias.parent_session_id.clone();
event.subagent_id = Some(alias.subagent_id.clone());
event.metadata = merge_metadata(event.metadata.clone(), metadata);
}
fn messages_user_task_text(payload: &Value) -> Option<String> {
payload
.get("messages")
.and_then(Value::as_array)
.and_then(|messages| messages.iter().rev().find_map(user_message_task_text))
}
fn responses_user_task_text(payload: &Value) -> Option<String> {
payload
.get("input")
.and_then(responses_input_task_text)
.or_else(|| payload.get("prompt").and_then(prompt_task_text))
}
fn affinity_key_from_task_text(task_text: String) -> Option<String> {
let normalized = normalize_affinity_text(&task_text);
(normalized.chars().count() >= REQUEST_AFFINITY_KEY_MIN_CHARS)
.then(|| truncate_affinity_text(&normalized, REQUEST_AFFINITY_KEY_MAX_CHARS))
}
fn user_message_task_text(message: &Value) -> Option<String> {
if message.get("role").and_then(Value::as_str) != Some("user") {
return None;
}
content_task_text(message.get("content")?)
}
fn responses_input_task_text(input: &Value) -> Option<String> {
match input {
Value::String(text) => affinity_candidate_text(text),
Value::Array(items) => items.iter().rev().find_map(user_message_task_text),
_ => None,
}
}
fn prompt_task_text(prompt: &Value) -> Option<String> {
prompt.as_str().and_then(affinity_candidate_text)
}
fn content_task_text(content: &Value) -> Option<String> {
match content {
Value::String(text) => affinity_candidate_text(text),
Value::Array(blocks) => blocks.iter().rev().find_map(content_block_task_text),
_ => None,
}
}
fn content_block_task_text(block: &Value) -> Option<String> {
if let Some(block_type) = block.get("type").and_then(Value::as_str)
&& !matches!(block_type, "text" | "input_text")
{
return None;
}
block
.get("text")
.and_then(Value::as_str)
.and_then(affinity_candidate_text)
}
fn affinity_candidate_text(text: &str) -> Option<String> {
let cleaned = text.trim();
if cleaned.is_empty() || looks_like_json_payload(cleaned) {
return None;
}
Some(cleaned.to_string())
}
fn looks_like_json_payload(text: &str) -> bool {
let trimmed = text.trim_start();
if !matches!(trimmed.chars().next(), Some('{' | '[')) {
return false;
}
matches!(
serde_json::from_str::<Value>(trimmed),
Ok(Value::Object(_) | Value::Array(_))
)
}
fn normalize_affinity_text(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn truncate_affinity_text(text: &str, max_chars: usize) -> String {
text.chars().take(max_chars).collect()
}
pub(crate) fn insert_optional(object: &mut Map<String, Value>, key: &str, value: Option<&str>) {
if let Some(value) = value {
object.insert(key.to_string(), json!(value));
}
}
pub(crate) fn merge_metadata(left: Value, right: Value) -> Value {
match (left, right) {
(Value::Object(mut left), Value::Object(right)) => {
for (key, value) in right {
if !value.is_null() {
left.insert(key, value);
}
}
Value::Object(left)
}
(Value::Null, right) => right,
(left, Value::Null) => left,
(left, right) => {
let mut object = Map::new();
object.insert("metadata".into(), left);
object.insert("extra_metadata".into(), right);
Value::Object(object)
}
}
}
#[cfg(test)]
#[path = "../../../tests/coverage/agents/alignment_tests.rs"]
mod tests;