use axum::http::{request::Parts, HeaderMap, Method};
pub const OPENAI_BASE: &str = "https://api.openai.com";
pub const CHATGPT_BASE: &str = "https://chatgpt.com/backend-api/codex";
const CHATGPT_ACCOUNT_HEADER: &str = "chatgpt-account-id";
const ORIGINATOR_HEADER: &str = "originator";
const CODEX_ORIGINATOR_PREFIX: &str = "codex";
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
ChatGptSubscription,
Platform,
}
impl AuthMode {
pub fn resolve(headers: &HeaderMap) -> Self {
if headers.contains_key(CHATGPT_ACCOUNT_HEADER) {
Self::ChatGptSubscription
} else {
Self::Platform
}
}
}
fn is_codex_request(headers: &HeaderMap) -> bool {
headers.contains_key(CHATGPT_ACCOUNT_HEADER)
|| headers
.get(ORIGINATOR_HEADER)
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.starts_with(CODEX_ORIGINATOR_PREFIX))
}
const MAX_DECLARED_SESSION_ID: usize = 128;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireFormat {
AnthropicMessages,
OpenAiResponses,
Unknown,
}
impl WireFormat {
pub const ALL: [WireFormat; 3] = [
Self::AnthropicMessages,
Self::OpenAiResponses,
Self::Unknown,
];
pub fn resolve(parts: &Parts) -> Self {
if parts.method != Method::POST {
return Self::Unknown;
}
match parts.uri.path() {
"/v1/messages" => Self::AnthropicMessages,
"/v1/responses" => Self::OpenAiResponses,
_ => Self::Unknown,
}
}
pub fn resolve_upstream(parts: &Parts) -> Self {
match Self::resolve(parts) {
Self::Unknown if is_codex_request(&parts.headers) => Self::OpenAiResponses,
resolved => resolved,
}
}
pub fn is_captured(self) -> bool {
!matches!(self, Self::Unknown)
}
pub fn as_str(self) -> &'static str {
match self {
Self::AnthropicMessages => "anthropic-messages",
Self::OpenAiResponses => "openai-responses",
Self::Unknown => "unknown",
}
}
pub fn provider(self) -> &'static str {
match self {
Self::AnthropicMessages => "anthropic",
Self::OpenAiResponses => "openai",
Self::Unknown => "unknown",
}
}
pub fn default_upstream(self) -> &'static str {
match self {
Self::AnthropicMessages => super::ANTHROPIC_BASE,
Self::OpenAiResponses => OPENAI_BASE,
Self::Unknown => super::ANTHROPIC_BASE,
}
}
pub fn has_decoder(self) -> bool {
matches!(self, Self::AnthropicMessages | Self::OpenAiResponses)
}
pub fn transforms_apply(self) -> bool {
matches!(self, Self::AnthropicMessages)
}
}
pub fn declared_session_id(fmt: WireFormat, body: &serde_json::Value) -> Option<String> {
match fmt {
WireFormat::AnthropicMessages => anthropic_declared_session_id(body),
WireFormat::OpenAiResponses => codex_declared_session_id(body),
WireFormat::Unknown => None,
}
}
fn usable_session_id(sid: &str) -> Option<String> {
let sid = sid.trim();
if sid.is_empty() || sid.len() > MAX_DECLARED_SESSION_ID {
return None;
}
if sid.chars().any(char::is_control) {
return None;
}
Some(sid.to_string())
}
fn anthropic_declared_session_id(body: &serde_json::Value) -> Option<String> {
let raw = body.get("metadata")?.get("user_id")?.as_str()?;
if raw.len() > 4096 {
return None;
}
let parsed: serde_json::Value = serde_json::from_str(raw).ok()?;
usable_session_id(parsed.get("session_id")?.as_str()?)
}
fn codex_declared_session_id(body: &serde_json::Value) -> Option<String> {
usable_session_id(body.get("client_metadata")?.get("session_id")?.as_str()?)
}
pub struct ConversationView<'a> {
pub user_turns: Vec<&'a serde_json::Value>,
pub text_block_type: &'static str,
pub tool_result_ids: Vec<String>,
}
pub fn conversation_view(
fmt: WireFormat,
body: &serde_json::Value,
) -> Option<ConversationView<'_>> {
match fmt {
WireFormat::AnthropicMessages => anthropic_conversation_view(body),
WireFormat::OpenAiResponses => responses_conversation_view(body),
WireFormat::Unknown => None,
}
}
fn anthropic_conversation_view(body: &serde_json::Value) -> Option<ConversationView<'_>> {
let messages = body.get("messages")?.as_array()?;
let user_turns: Vec<&serde_json::Value> = messages
.iter()
.filter(|m| m.get("role").and_then(|r| r.as_str()) == Some("user"))
.collect();
let mut tool_result_ids = Vec::new();
if let Some(blocks) = user_turns
.last()
.and_then(|last| last.get("content"))
.and_then(|c| c.as_array())
{
for b in blocks {
if b.get("type").and_then(|t| t.as_str()) != Some("tool_result") {
continue;
}
if let Some(id) = b.get("tool_use_id").and_then(|v| v.as_str()) {
if !id.is_empty() {
tool_result_ids.push(id.to_string());
}
}
}
}
Some(ConversationView {
user_turns,
text_block_type: "text",
tool_result_ids,
})
}
fn responses_conversation_view(body: &serde_json::Value) -> Option<ConversationView<'_>> {
let items = body.get("input")?.as_array()?;
let user_turns: Vec<&serde_json::Value> = items
.iter()
.filter(|m| {
matches!(
m.get("type").and_then(|t| t.as_str()),
None | Some("message")
) && m.get("role").and_then(|r| r.as_str()) == Some("user")
})
.collect();
let mut tool_result_ids: Vec<String> = Vec::new();
for item in items.iter().rev() {
if tool_result_ids.len() >= super::session::RECENT_TOOL_USE_IDS {
break;
}
let Some(id) = item.get("call_id").and_then(|v| v.as_str()) else {
continue;
};
if id.is_empty() || tool_result_ids.iter().any(|k| k == id) {
continue;
}
tool_result_ids.push(id.to_string());
}
Some(ConversationView {
user_turns,
text_block_type: "input_text",
tool_result_ids,
})
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Request;
fn parts_of(method: Method, uri: &str) -> Parts {
let req = Request::builder()
.method(method)
.uri(uri)
.body(())
.expect("build request");
req.into_parts().0
}
fn parts_with(method: Method, uri: &str, headers: &[(&str, &str)]) -> Parts {
let mut req = Request::builder().method(method).uri(uri);
for (k, v) in headers {
req = req.header(*k, *v);
}
req.body(()).expect("build request").into_parts().0
}
#[test]
fn auth_mode_reads_the_account_header_and_nothing_else() {
assert_eq!(
AuthMode::resolve(
&parts_with(
Method::POST,
"/v1/responses",
&[("chatgpt-account-id", "ef1a0c98-317c-4a79-9ed0-9e75361027ed")],
)
.headers
),
AuthMode::ChatGptSubscription
);
assert_eq!(
AuthMode::resolve(
&parts_with(
Method::POST,
"/v1/responses",
&[("authorization", "Bearer sk-proj-abc")],
)
.headers
),
AuthMode::Platform
);
assert_eq!(
AuthMode::resolve(&parts_of(Method::POST, "/v1/responses").headers),
AuthMode::Platform
);
}
#[test]
fn an_uncaptured_codex_route_follows_codex_upstream() {
let models = parts_with(
Method::GET,
"/v1/models?client_version=0.150.1",
&[("originator", "codex_exec")],
);
assert_eq!(WireFormat::resolve(&models), WireFormat::Unknown);
assert_eq!(
WireFormat::resolve_upstream(&models),
WireFormat::OpenAiResponses
);
let with_account = parts_with(
Method::GET,
"/v1/models",
&[("chatgpt-account-id", "ef1a0c98")],
);
assert_eq!(
WireFormat::resolve_upstream(&with_account),
WireFormat::OpenAiResponses
);
}
#[test]
fn an_unmarked_uncaptured_route_still_resolves_unknown() {
for headers in [
&[][..],
&[("authorization", "Bearer sk-ant-oat01-abc")][..],
&[("x-api-key", "sk-ant-api03-abc")][..],
&[("originator", "not-codex")][..],
] {
let parts = parts_with(Method::GET, "/v1/models", headers);
assert_eq!(
WireFormat::resolve_upstream(&parts),
WireFormat::Unknown,
"unmarked route promoted with headers {headers:?}"
);
}
}
#[test]
fn promotion_never_widens_what_is_captured() {
let promoted = parts_with(Method::GET, "/v1/models", &[("originator", "codex_cli_rs")]);
assert!(!WireFormat::resolve(&promoted).is_captured());
let turn = parts_with(
Method::POST,
"/v1/responses",
&[("originator", "codex_exec")],
);
assert_eq!(
WireFormat::resolve_upstream(&turn),
WireFormat::resolve(&turn)
);
}
#[test]
fn resolve_keys_on_route_and_method_only() {
assert_eq!(
WireFormat::resolve(&parts_of(Method::POST, "/v1/messages")),
WireFormat::AnthropicMessages
);
assert_eq!(
WireFormat::resolve(&parts_of(Method::POST, "/v1/responses")),
WireFormat::OpenAiResponses
);
assert_eq!(
WireFormat::resolve(&parts_of(Method::GET, "/v1/responses")),
WireFormat::Unknown
);
assert_eq!(
WireFormat::resolve(&parts_of(Method::POST, "/v1/messages/count_tokens")),
WireFormat::Unknown
);
}
#[test]
fn all_names_every_variant_exactly_once() {
let names: Vec<_> = WireFormat::ALL.iter().map(|f| f.as_str()).collect();
assert_eq!(
names,
vec!["anthropic-messages", "openai-responses", "unknown"]
);
}
#[test]
fn unknown_forwards_to_the_anthropic_base_as_it_does_today() {
assert_eq!(
WireFormat::Unknown.default_upstream(),
crate::boundary::ANTHROPIC_BASE
);
assert_eq!(WireFormat::OpenAiResponses.default_upstream(), OPENAI_BASE);
}
fn codex_turn() -> serde_json::Value {
serde_json::json!({
"model": "gpt-5-codex",
"input": [
{"type": "message", "id": "msg_1", "role": "developer",
"content": [{"type": "input_text", "text": "<skills_instructions>…"}]},
{"type": "message", "id": "msg_2", "role": "user",
"content": [{"type": "input_text", "text": "<environment_context>\n <cwd>/repo</cwd>\n</environment_context>"}]},
{"type": "message", "id": "msg_3", "role": "user",
"content": [{"type": "input_text", "text": "build the parser"}]},
{"type": "function_call", "id": "fc_1", "call_id": "call_aaa",
"name": "shell", "arguments": "{}"},
{"type": "function_call_output", "id": "fco_1", "call_id": "call_aaa",
"output": "ok"}
]
})
}
#[test]
fn the_responses_view_reads_input_and_leaves_the_preamble_alone() {
let body = codex_turn();
let view = conversation_view(WireFormat::OpenAiResponses, &body).expect("a view");
assert_eq!(view.user_turns.len(), 2);
assert_eq!(
view.user_turns[1]["content"][0]["text"], "build the parser",
"the typed prompt is the last user turn"
);
assert_eq!(view.text_block_type, "input_text");
assert_eq!(view.tool_result_ids, vec!["call_aaa".to_string()]);
}
#[test]
fn responses_call_ids_are_newest_first_deduped_and_bounded() {
let mut items = Vec::new();
for i in 0..40 {
items.push(serde_json::json!({
"type": "function_call", "call_id": format!("call_{i:02}"), "name": "shell"
}));
items.push(serde_json::json!({
"type": "function_call_output", "call_id": format!("call_{i:02}"), "output": "ok"
}));
}
let body = serde_json::json!({ "input": items });
let view = conversation_view(WireFormat::OpenAiResponses, &body).expect("a view");
assert_eq!(
view.tool_result_ids.len(),
super::super::session::RECENT_TOOL_USE_IDS
);
assert_eq!(
view.tool_result_ids[0], "call_39",
"newest first — the request's newest ids are the ones still in the registry"
);
assert_eq!(view.tool_result_ids[7], "call_32");
}
#[test]
fn every_responses_item_that_carries_an_id_names_it_call_id() {
let body = serde_json::json!({"input": [
{"type": "custom_tool_call_output", "call_id": "call_custom", "output": "ok"},
{"type": "mcp_tool_call_output", "call_id": "call_mcp", "output": {}},
{"type": "tool_search_output", "call_id": "call_search", "status": "ok"},
{"type": "a_variant_that_does_not_exist_yet", "call_id": "call_future"}
]});
let view = conversation_view(WireFormat::OpenAiResponses, &body).expect("a view");
assert_eq!(
view.tool_result_ids,
vec!["call_future", "call_search", "call_mcp", "call_custom"]
);
}
#[test]
fn each_format_reads_only_its_own_body_shape() {
let codex = codex_turn();
let claude = serde_json::json!({"messages": [
{"role": "user", "content": [{"type": "text", "text": "build the parser"}]}
]});
assert!(conversation_view(WireFormat::AnthropicMessages, &codex).is_none());
assert!(conversation_view(WireFormat::OpenAiResponses, &claude).is_none());
assert!(conversation_view(WireFormat::Unknown, &codex).is_none());
assert!(conversation_view(WireFormat::Unknown, &claude).is_none());
}
#[test]
fn the_messages_view_is_unchanged_by_the_move() {
let body = serde_json::json!({"messages": [
{"role": "user", "content": [{"type": "text", "text": "build the parser"}]},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_old", "name": "Bash", "input": {}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_old", "content": "ok"}
]},
{"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_new", "name": "Bash", "input": {}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_new", "content": "ok"},
{"type": "text", "text": "and now the lexer"}
]}
]});
let view = conversation_view(WireFormat::AnthropicMessages, &body).expect("a view");
assert_eq!(view.user_turns.len(), 3);
assert_eq!(view.text_block_type, "text");
assert_eq!(view.tool_result_ids, vec!["toolu_new".to_string()]);
}
#[test]
fn each_format_reads_only_its_own_declaration() {
let claude = serde_json::json!({
"metadata": {"user_id": r#"{"session_id":"5c7d9833"}"#}
});
let codex = serde_json::json!({
"client_metadata": {
"session_id": "01a07677-988f-7901-af48-225c7386aabc",
"thread_id": "01a07677-988f-7901-af48-225c7386aabc",
"turn_id": "01a07677-7334-74a2-be87-bdd0bbe8fb77",
"x-codex-installation-id": "5532dc79-4512-4bae-ab73-0dd3e814d329"
}
});
assert_eq!(
declared_session_id(WireFormat::AnthropicMessages, &claude).as_deref(),
Some("5c7d9833")
);
assert_eq!(
declared_session_id(WireFormat::OpenAiResponses, &claude),
None
);
assert_eq!(
declared_session_id(WireFormat::OpenAiResponses, &codex).as_deref(),
Some("01a07677-988f-7901-af48-225c7386aabc")
);
assert_eq!(
declared_session_id(WireFormat::AnthropicMessages, &codex),
None
);
assert_eq!(declared_session_id(WireFormat::Unknown, &claude), None);
assert_eq!(declared_session_id(WireFormat::Unknown, &codex), None);
}
#[test]
fn codex_reads_session_id_and_not_a_neighbouring_field() {
let body = serde_json::json!({
"client_metadata": {"session_id": "sess-real", "turn_id": "turn-other",
"thread_id": "thread-other"}
});
assert_eq!(
declared_session_id(WireFormat::OpenAiResponses, &body).as_deref(),
Some("sess-real")
);
let cache_key_only = serde_json::json!({
"prompt_cache_key": "01a07677-988f-7901-af48-225c7386aabc"
});
assert_eq!(
declared_session_id(WireFormat::OpenAiResponses, &cache_key_only),
None
);
}
#[test]
fn a_codex_declaration_that_is_not_usable_is_silently_ignored() {
let cases = [
serde_json::json!({}), serde_json::json!({"client_metadata": {}}), serde_json::json!({"client_metadata": {"session_id": 42}}), serde_json::json!({"client_metadata": {"session_id": ""}}), serde_json::json!({"client_metadata": {"session_id": " "}}), serde_json::json!({"client_metadata": {"session_id": "a\u{0000}b"}}), serde_json::json!({"client_metadata": {"session_id": "x".repeat(500)}}), ];
for body in cases {
assert_eq!(
declared_session_id(WireFormat::OpenAiResponses, &body),
None,
"must not trust {body}"
);
}
}
}