tokn-headers 0.2.0

Header schema, parsing, and persona utilities shared across tokn crates
Documentation
use crate::{HeaderMap, TemplateVars};
use smol_str::SmolStr;

pub const SESSION_ID_HEADERS: &[&str] = &[
  "x-session-id",
  "x-client-session-id",
  "session-id",
  "session_id",
  "x-session-affinity",
  "x-opencode-session",
];

pub const THREAD_ID_HEADERS: &[&str] = &["thread-id", "thread_id"];

pub const PARENT_THREAD_ID_HEADERS: &[&str] = &[
  "x-codex-parent-thread-id",
  "x-parent-thread-id",
  "parent-thread-id",
  "parent_thread_id",
];

pub const REQUEST_ID_HEADERS: &[&str] = &["x-request-id", "x-interaction-id", "x-opencode-request"];

pub const PROJECT_ID_HEADERS: &[&str] = &["x-opencode-project", "x-project-cwd"];

pub const INTERACTION_ID_HEADERS: &[&str] = &["x-interaction-id"];

pub const ACCOUNT_ID_HEADERS: &[&str] = &["chatgpt-account-id"];

pub fn first_present<'a>(headers: &'a HeaderMap, names: &[&str]) -> Option<&'a str> {
  names.iter().find_map(|name| {
    headers
      .get(*name)
      .map(|value| value.as_str().trim())
      .filter(|value| !value.is_empty())
  })
}

pub fn first_present_smol(headers: &HeaderMap, names: &[&str]) -> Option<SmolStr> {
  first_present(headers, names).map(SmolStr::new)
}

pub fn build_template_vars(headers: &HeaderMap) -> TemplateVars {
  TemplateVars {
    session_id: first_present_smol(headers, SESSION_ID_HEADERS),
    request_id: first_present_smol(headers, REQUEST_ID_HEADERS),
    project_cwd: first_present_smol(headers, PROJECT_ID_HEADERS),
    interaction_id: first_present_smol(headers, INTERACTION_ID_HEADERS),
    account_id: first_present_smol(headers, ACCOUNT_ID_HEADERS),
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use crate::{HeaderName, HeaderValue};

  fn header_map(pairs: &[(&str, &str)]) -> HeaderMap {
    let mut headers = HeaderMap::new();
    for (name, value) in pairs {
      headers.insert(HeaderName::new(*name), HeaderValue::from_string((*value).to_string()));
    }
    headers
  }

  #[test]
  fn first_present_uses_priority_and_trims() {
    let headers = header_map(&[
      ("x-session-id", "   "),
      ("x-client-session-id", "   "),
      ("session-id", " sess-2 "),
      ("x-opencode-session", "sess-3"),
    ]);

    assert_eq!(first_present(&headers, SESSION_ID_HEADERS), Some("sess-2"));
  }

  #[test]
  fn template_vars_cover_all_correlation_headers() {
    let headers = header_map(&[
      ("x-session-id", "ses_abc"),
      ("x-request-id", "req_xyz"),
      ("x-project-cwd", "/tmp/work"),
      ("x-interaction-id", "int_9"),
      ("chatgpt-account-id", "acct_42"),
    ]);

    let vars = build_template_vars(&headers);
    assert_eq!(vars.session_id.as_deref(), Some("ses_abc"));
    assert_eq!(vars.request_id.as_deref(), Some("req_xyz"));
    assert_eq!(vars.project_cwd.as_deref(), Some("/tmp/work"));
    assert_eq!(vars.interaction_id.as_deref(), Some("int_9"));
    assert_eq!(vars.account_id.as_deref(), Some("acct_42"));
  }

  #[test]
  fn thread_headers_keep_thread_topology_separate_from_session_identity() {
    let headers = header_map(&[
      ("session-id", "session-root"),
      ("thread-id", "thread-child"),
      ("x-codex-parent-thread-id", "thread-root"),
    ]);

    assert_eq!(first_present(&headers, SESSION_ID_HEADERS), Some("session-root"));
    assert_eq!(first_present(&headers, THREAD_ID_HEADERS), Some("thread-child"));
    assert_eq!(first_present(&headers, PARENT_THREAD_ID_HEADERS), Some("thread-root"));
  }
}