diff --git a/src/proxy.rs b/src/proxy.rs index ca95ec7..3132b19 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -10,6 +10,16 @@ use serde_json::Value; const UPSTREAM: &str = "https://api.anthropic.com"; +/// Extract the session UUID from `metadata.user_id`. Claude Code ≥2.1.x +/// sends a JSON-ish blob containing `"session_id":""`; older builds +/// used `user__account__session_`. +fn session_key(u: &str) -> Option<&str> { + if let Some(rest) = u.split(r#"session_id":""#).nth(1) { + return rest.split('"').next(); + } + u.split("session_").nth(1) +} + #[derive(Clone)] struct Ctx { client: reqwest::Client, @@ -66,7 +76,7 @@ async fn forward_inner(ctx: Ctx, req: Request) -> anyhow::Result { let key = v .pointer("/metadata/user_id") .and_then(Value::as_str) - .and_then(|u| u.split("session_").nth(1)) + .and_then(session_key) .unwrap_or("unknown") .to_string(); // Tool results ride along in the request body; surface them @@ -127,3 +137,23 @@ async fn forward_inner(ctx: Ctx, req: Request) -> anyhow::Result { }; Ok(builder.body(body)?) } + +#[cfg(test)] +mod tests { + use super::session_key; + + #[test] + fn session_key_handles_both_user_id_formats() { + // Claude Code ≥2.1.x: JSON blob + assert_eq!( + session_key(r#"{"device_id":"d","account_uuid":"a","session_id":"a78d91b6-1234-5678-9abc-def012345678"}"#), + Some("a78d91b6-1234-5678-9abc-def012345678") + ); + // Legacy underscore format + assert_eq!( + session_key("user_abc123_account_acc-uuid_session_29bd3436-aaaa-bbbb-cccc-111122223333"), + Some("29bd3436-aaaa-bbbb-cccc-111122223333") + ); + assert_eq!(session_key("no session here"), None); + } +}