use std::io::{BufRead, BufReader};
use axum::http::HeaderMap;
use serde_json::{Map, Value, json};
use crate::alignment::{
GatewayRouteKind, SessionAlias, insert_optional, json_string_at, merge_metadata,
};
use crate::model::{AgentKind, SessionEvent, SubagentEvent};
const CHATGPT_CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex";
#[derive(Debug, Clone)]
pub(crate) struct SubagentContext {
pub(crate) parent_session_id: String,
nickname: Option<String>,
role: Option<String>,
depth: Option<String>,
}
pub(crate) fn owns_gateway_provider(provider: &str) -> bool {
provider == "openai.responses"
}
pub(crate) fn prompt_cache_session_id(body: &Value, route: GatewayRouteKind) -> Option<String> {
if route != GatewayRouteKind::OpenAiResponses {
return None;
}
let has_codex_metadata = body
.get("client_metadata")
.and_then(|metadata| metadata.get("x-codex-installation-id"))
.and_then(Value::as_str)
.is_some_and(|value| !value.is_empty());
if !has_codex_metadata {
return None;
}
json_string_at(body, &[&["prompt_cache_key"][..]])
}
pub(crate) fn chatgpt_upstream_url_if_needed(
headers: &HeaderMap,
route: GatewayRouteKind,
path_and_query: &str,
has_replacement_key: bool,
) -> Option<String> {
(is_openai_route(route) && has_chatgpt_oauth_jwt(headers) && !has_replacement_key)
.then(|| chatgpt_upstream_url(path_and_query))
}
pub(crate) fn strip_chatgpt_oauth_for_openai_route(
headers: &HeaderMap,
route: GatewayRouteKind,
has_replacement_key: bool,
) -> HeaderMap {
if !is_openai_route(route) || !has_replacement_key {
return headers.clone();
}
let mut out = headers.clone();
if has_chatgpt_oauth_jwt(&out) {
out.remove(http::header::AUTHORIZATION);
}
out
}
fn chatgpt_upstream_url(path_and_query: &str) -> String {
let path = path_and_query.strip_prefix("/v1").unwrap_or(path_and_query);
format!("{CHATGPT_CODEX_BASE_URL}{path}")
}
fn has_chatgpt_oauth_jwt(headers: &HeaderMap) -> bool {
headers
.get(http::header::AUTHORIZATION)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("Bearer eyJ"))
}
fn is_openai_route(route: GatewayRouteKind) -> bool {
matches!(
route,
GatewayRouteKind::OpenAiResponses
| GatewayRouteKind::OpenAiChatCompletions
| GatewayRouteKind::OpenAiModels
)
}
pub(crate) async fn subagent_context(event: &SessionEvent) -> Option<SubagentContext> {
if event.agent_kind != AgentKind::Codex {
return None;
}
let context = match subagent_context_from_value(&event.payload)
.or_else(|| subagent_context_from_value(&event.metadata))
{
Some(context) => Some(context),
None => subagent_context_from_transcript(event).await,
};
context.filter(|context| context.parent_session_id != event.session_id)
}
async fn subagent_context_from_transcript(event: &SessionEvent) -> Option<SubagentContext> {
let transcript_path = json_string_at(&event.metadata, &[&["transcript_path"][..]])
.or_else(|| json_string_at(&event.payload, &[&["transcript_path"][..]]))?;
tokio::task::spawn_blocking(move || {
let file = std::fs::File::open(transcript_path).ok()?;
let mut reader = BufReader::new(file);
let mut line = String::new();
if reader.read_line(&mut line).ok()? == 0 {
return None;
}
let value = serde_json::from_str::<Value>(&line).ok()?;
subagent_context_from_value(&value)
})
.await
.ok()
.flatten()
}
fn subagent_context_from_value(value: &Value) -> Option<SubagentContext> {
let parent_session_id = json_string_at(
value,
&[
&["source", "subagent", "thread_spawn", "parent_thread_id"][..],
&[
"payload",
"source",
"subagent",
"thread_spawn",
"parent_thread_id",
][..],
&[
"session_meta",
"payload",
"source",
"subagent",
"thread_spawn",
"parent_thread_id",
][..],
&[
"extra",
"source",
"subagent",
"thread_spawn",
"parent_thread_id",
][..],
],
)?;
Some(SubagentContext {
parent_session_id,
nickname: json_string_at(
value,
&[
&["agent_nickname"][..],
&["payload", "agent_nickname"][..],
&["session_meta", "payload", "agent_nickname"][..],
&["source", "subagent", "thread_spawn", "agent_nickname"][..],
&[
"payload",
"source",
"subagent",
"thread_spawn",
"agent_nickname",
][..],
&[
"session_meta",
"payload",
"source",
"subagent",
"thread_spawn",
"agent_nickname",
][..],
],
),
role: json_string_at(
value,
&[
&["agent_role"][..],
&["payload", "agent_role"][..],
&["session_meta", "payload", "agent_role"][..],
&["source", "subagent", "thread_spawn", "agent_role"][..],
&[
"payload",
"source",
"subagent",
"thread_spawn",
"agent_role",
][..],
&[
"session_meta",
"payload",
"source",
"subagent",
"thread_spawn",
"agent_role",
][..],
],
),
depth: json_string_at(
value,
&[
&["source", "subagent", "thread_spawn", "depth"][..],
&["payload", "source", "subagent", "thread_spawn", "depth"][..],
&[
"session_meta",
"payload",
"source",
"subagent",
"thread_spawn",
"depth",
][..],
],
),
})
}
pub(crate) fn augment_subagent_metadata(metadata: Value, context: &SubagentContext) -> Value {
let mut object = Map::new();
object.insert("thread_source".into(), json!("subagent"));
object.insert(
"codex_parent_thread_id".into(),
json!(context.parent_session_id.clone()),
);
insert_optional(&mut object, "agent_nickname", context.nickname.as_deref());
insert_optional(&mut object, "agent_role", context.role.as_deref());
insert_optional(
&mut object,
"codex_subagent_depth",
context.depth.as_deref(),
);
merge_metadata(metadata, Value::Object(object))
}
pub(crate) fn subagent_start_event(
event: &SessionEvent,
context: &SubagentContext,
) -> SubagentEvent {
SubagentEvent {
session_id: context.parent_session_id.clone(),
agent_kind: event.agent_kind,
event_name: event.event_name.clone(),
subagent_id: event.session_id.clone(),
payload: event.payload.clone(),
metadata: merge_metadata(
event.metadata.clone(),
json!({ "codex_subagent_session_id": event.session_id.clone() }),
),
}
}
pub(crate) fn alias_for_child_session(
child_session_id: String,
context: &SubagentContext,
) -> SessionAlias {
SessionAlias::new(
context.parent_session_id.clone(),
child_session_id.clone(),
json!({
"thread_source": "subagent",
"codex_parent_thread_id": context.parent_session_id.clone(),
"codex_subagent_session_id": child_session_id,
}),
)
}
pub(crate) fn llm_owner_metadata(scope_metadata: Option<&Value>) -> Value {
let Some(Value::Object(scope_metadata)) = scope_metadata else {
return Value::Null;
};
let mut metadata = Map::new();
for key in [
"thread_source",
"codex_parent_thread_id",
"codex_subagent_session_id",
"codex_subagent_depth",
"agent_nickname",
"agent_role",
] {
if let Some(value) = scope_metadata.get(key)
&& !value.is_null()
{
metadata.insert(key.to_string(), value.clone());
}
}
if metadata.is_empty() {
Value::Null
} else {
Value::Object(metadata)
}
}
#[cfg(test)]
#[path = "../../tests/coverage/alignment_codex_tests.rs"]
mod tests;