use std::collections::HashMap;
use axum::http::HeaderMap;
use nemo_relay::api::llm::LlmRequest;
use serde_json::{Map, Value, json};
use crate::config::header_string;
use crate::model::{AgentKind, LlmEvent, NormalizedEvent, SessionEvent, SubagentEvent, ToolEvent};
pub(crate) mod claude_code;
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,
}
#[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> {
header_string(headers, "x-nemo-relay-session-id")
.or_else(|| claude_code::session_id_from_headers(headers))
.or_else(|| codex::prompt_cache_session_id(body, route))
}
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_oauth_for_openai_route(headers, route, has_openai_replacement_key)
}
pub(crate) fn gateway_subagent_id(headers: &HeaderMap) -> Option<String> {
header_string(headers, "x-nemo-relay-subagent-id")
}
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 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(request: &LlmRequest) -> Option<String> {
let task_text = request_user_task_text(&request.content)?;
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))
}
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,
&[
&["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::LlmStarted(mut event) => {
route_llm_event(&mut event, &alias, metadata);
(NormalizedEvent::LlmStarted(event), None)
}
NormalizedEvent::LlmEnded(mut event) => {
route_llm_event(&mut event, &alias, metadata);
(NormalizedEvent::LlmEnded(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_llm_event(event: &mut LlmEvent, alias: &SessionAlias, metadata: Value) {
event.session_id = alias.parent_session_id.clone();
event.metadata = merge_metadata(
event.metadata.clone(),
merge_metadata(
metadata,
json!({
"llm_correlation_status": "session_alias",
"llm_correlation_source": "session_alias",
"llm_correlation_subagent_id": alias.subagent_id.clone(),
}),
),
);
}
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 request_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))
.or_else(|| responses_input_task_text(payload.get("input")?))
.or_else(|| prompt_task_text(payload.get("prompt")?))
}
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 json_string_at(payload: &Value, paths: &[&[&str]]) -> Option<String> {
json_value_at(payload, paths)
.and_then(|value| match value {
Value::String(value) => Some(value),
Value::Number(value) => Some(value.to_string()),
Value::Bool(value) => Some(value.to_string()),
_ => None,
})
.filter(|value| !value.is_empty())
}
pub(crate) fn json_value_at(payload: &Value, paths: &[&[&str]]) -> Option<Value> {
paths.iter().find_map(|path| {
let mut current = payload;
for key in *path {
current = current.get(*key)?;
}
Some(current.clone())
})
}
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/alignment_tests.rs"]
mod tests;