//! Proxy Session - 请求会话管理 //! //! 为每个代理请求创建会话上下文,在整个请求生命周期中跟踪状态和元数据。 //! //! ## Session ID 提取 //! //! 支持从客户端请求中提取 Session ID,用于关联同一对话的多个请求: //! - Claude: 从 `metadata.user_id` (格式: `user_xxx_session_yyy`) 或 `metadata.session_id` 提取 //! - Codex: 从 headers 中的 `session_id` / `x-session-id` 或 `metadata.session_id` 提取 //! - 其他: 生成新的 UUID use axum::http::HeaderMap; use uuid::Uuid; // ============================================================================ // Session ID 提取器 // ============================================================================ /// Session ID 来源 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionIdSource { /// 从 metadata.user_id 提取 (Claude) MetadataUserId, /// 从 metadata.session_id 提取 MetadataSessionId, /// 从 headers 提取 (Codex) Header, /// 新生成 Generated, } /// Session ID 提取结果 #[derive(Debug, Clone)] pub struct SessionIdResult { /// 提取或生成的 Session ID pub session_id: String, /// Session ID 来源 pub source: SessionIdSource, /// 是否为客户端提供的 ID(非新生成) pub client_provided: bool, } /// 从请求中提取或生成 Session ID /// /// 轻量化实现,仅提取 session_id 用于日志记录,不做复杂的 Session 管理。 /// /// ## 提取优先级 /// /// ### Claude 请求 /// 1. `metadata.user_id` (格式: `user_xxx_session_yyy`) → 提取 `yyy` 部分 /// 2. `metadata.session_id` → 直接使用 /// 3. 生成新 UUID /// /// ### Codex 请求 /// 1. Headers: `session_id` 或 `x-session-id` /// 2. `metadata.session_id` /// 3. 生成新 UUID /// /// ## 示例 /// /// ```ignore /// let result = extract_session_id(&headers, &body, "claude"); /// println!("Session ID: {} (from {:?})", result.session_id, result.source); /// ``` pub fn extract_session_id( headers: &HeaderMap, body: &serde_json::Value, client_format: &str, ) -> SessionIdResult { if client_format == "claude" { if let Some(result) = extract_claude_session(headers, body) { return result; } } // Codex 请求特殊处理 if client_format == "codex" || client_format == "openai" { if let Some(result) = extract_codex_session(headers, body) { return result; } } // Claude 请求:从 metadata 提取 if let Some(result) = extract_from_metadata(body) { return result; } // 兜底:生成新 Session ID generate_new_session_id() } /// 提取 Claude Session ID fn extract_claude_session( headers: &HeaderMap, body: &serde_json::Value, ) -> Option { for header_name in &["x-claude-code-session-id", "claude-code-session-id"] { if let Some(value) = headers.get(*header_name) { if let Ok(session_id) = value.to_str() { if !session_id.is_empty() { return Some(SessionIdResult { session_id: session_id.to_string(), source: SessionIdSource::Header, client_provided: true, }); } } } } extract_from_metadata(body) } /// 提取 Codex Session ID fn extract_codex_session(headers: &HeaderMap, body: &serde_json::Value) -> Option { // 1. 从 headers 提取 for header_name in &["session_id", "x-session-id"] { if let Some(value) = headers.get(*header_name) { if let Ok(session_id) = value.to_str() { // Codex Session ID 通常较长(UUID 格式) if session_id.len() > 20 { return Some(SessionIdResult { session_id: format!("codex_{session_id}"), source: SessionIdSource::Header, client_provided: true, }); } } } } // 2. 从 body.metadata.session_id 提取 if let Some(session_id) = body .get("metadata") .and_then(|m| m.get("session_id")) .and_then(|v| v.as_str()) { if session_id.len() > 10 { return Some(SessionIdResult { session_id: format!("codex_{session_id}"), source: SessionIdSource::MetadataSessionId, client_provided: true, }); } } // previous_response_id 是 Responses 协议里的响应游标,不是稳定会话身份。 // Chat/Responses 桥接时该值通常来自上游每轮返回的随机 response id; // 若把它当 prompt_cache_key 或 Codex session header,会导致每轮请求换缓存 key。 None } /// 从 metadata 提取 Session ID (Claude) fn extract_from_metadata(body: &serde_json::Value) -> Option { let metadata = body.get("metadata")?; // 1. 从 metadata.user_id 提取(格式: user_xxx_session_yyy) if let Some(user_id) = metadata.get("user_id").and_then(|v| v.as_str()) { if let Some(session_id) = parse_session_from_user_id(user_id) { return Some(SessionIdResult { session_id, source: SessionIdSource::MetadataUserId, client_provided: true, }); } } // 2. 直接从 metadata.session_id 提取 if let Some(session_id) = metadata.get("session_id").and_then(|v| v.as_str()) { if !session_id.is_empty() { return Some(SessionIdResult { session_id: session_id.to_string(), source: SessionIdSource::MetadataSessionId, client_provided: true, }); } } None } /// 从 user_id 解析 session_id /// /// 格式: `user_identifier_session_actual_session_id` pub(super) fn parse_session_from_user_id(user_id: &str) -> Option { // 查找 "_session_" 分隔符 if let Some(pos) = user_id.find("_session_") { let session_id = &user_id[pos + 9..]; // "_session_" 长度为 9 if !session_id.is_empty() { return Some(session_id.to_string()); } } None } /// 生成新的 Session ID fn generate_new_session_id() -> SessionIdResult { SessionIdResult { session_id: Uuid::new_v4().to_string(), source: SessionIdSource::Generated, client_provided: false, } } #[cfg(test)] mod tests { use super::*; use serde_json::json; // ========== Session ID 提取测试 ========== #[test] fn test_extract_session_from_claude_metadata_user_id() { let headers = HeaderMap::new(); let body = json!({ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello"}], "metadata": { "user_id": "user_john_doe_session_abc123def456" } }); let result = extract_session_id(&headers, &body, "claude"); assert_eq!(result.session_id, "abc123def456"); assert_eq!(result.source, SessionIdSource::MetadataUserId); assert!(result.client_provided); } #[test] fn test_extract_session_from_claude_metadata_session_id() { let headers = HeaderMap::new(); let body = json!({ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello"}], "metadata": { "session_id": "my-session-123" } }); let result = extract_session_id(&headers, &body, "claude"); assert_eq!(result.session_id, "my-session-123"); assert_eq!(result.source, SessionIdSource::MetadataSessionId); assert!(result.client_provided); } #[test] fn test_extract_session_from_claude_header() { let mut headers = HeaderMap::new(); headers.insert( "x-claude-code-session-id", "d937243f-2702-4f20-97b6-c9682235ab81".parse().unwrap(), ); let body = json!({ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello"}] }); let result = extract_session_id(&headers, &body, "claude"); assert_eq!(result.session_id, "d937243f-2702-4f20-97b6-c9682235ab81"); assert_eq!(result.source, SessionIdSource::Header); assert!(result.client_provided); } #[test] fn test_extract_session_from_claude_header_precedes_metadata() { let mut headers = HeaderMap::new(); headers.insert( "x-claude-code-session-id", "header-session-123".parse().unwrap(), ); let body = json!({ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello"}], "metadata": { "session_id": "my-session-123" } }); let result = extract_session_id(&headers, &body, "claude"); assert_eq!(result.session_id, "header-session-123"); assert_eq!(result.source, SessionIdSource::Header); assert!(result.client_provided); } #[test] fn test_codex_previous_response_id_is_not_stable_session_identity() { let headers = HeaderMap::new(); let body = json!({ "input": "Write a function", "previous_response_id": "resp_abc123def456789" }); let result = extract_session_id(&headers, &body, "codex"); assert!(!result.session_id.is_empty()); assert_eq!(result.source, SessionIdSource::Generated); assert!(!result.client_provided); } #[test] fn test_extract_session_generates_new_when_not_found() { let headers = HeaderMap::new(); let body = json!({ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "Hello"}] }); let result = extract_session_id(&headers, &body, "claude"); assert!(!result.session_id.is_empty()); assert_eq!(result.source, SessionIdSource::Generated); assert!(!result.client_provided); } #[test] fn test_parse_session_from_user_id() { assert_eq!( parse_session_from_user_id("user_john_session_abc123"), Some("abc123".to_string()) ); assert_eq!( parse_session_from_user_id("my_app_session_xyz789"), Some("xyz789".to_string()) ); // 注意: "_session_" 是分隔符,所以下面的字符串会匹配 assert_eq!( parse_session_from_user_id("no_session_marker"), Some("marker".to_string()) ); // 没有 "_session_" 分隔符的情况 assert_eq!(parse_session_from_user_id("user_john_abc123"), None); assert_eq!(parse_session_from_user_id("_session_"), None); } }