diff --git a/gateway.mjs b/gateway.mjs index 44d3384..58ed1db 100644 --- a/gateway.mjs +++ b/gateway.mjs @@ -63,23 +63,44 @@ const REASONING_POINTERS = [ ]; const REQUEST_THREAD_ID_POINTERS = [ "/thread_id", + "/client_metadata/thread_id", + "/client_metadata/x-codex-thread-id", "/thread", "/thread/id", + "/threadId", + "/client_metadata/threadId", "/conversation_id", + "/conversationId", "/conversation", "/conversation/id", + "/metadata/thread_id", + "/metadata/threadId", + "/metadata/conversation_id", + "/metadata/conversationId", + "/x-codex-turn-metadata/thread_id", + "/x-codex-turn-metadata/threadId", + "/x-codex-turn-metadata/conversation_id", + "/x-codex-turn-metadata/conversationId", + "/previous_response_id", ]; const RESPONSE_THREAD_ID_POINTERS = [ "/thread_id", + "/client_metadata/thread_id", + "/client_metadata/x-codex-thread-id", "/thread", "/thread/id", + "/threadId", + "/client_metadata/threadId", "/conversation_id", + "/conversationId", "/conversation", "/conversation/id", "/response/thread_id", + "/response/threadId", "/response/thread", "/response/thread/id", "/response/conversation_id", + "/response/conversationId", "/response/conversation", "/response/conversation/id", ]; @@ -217,6 +238,38 @@ function extractResponseThreadId(payload) { return extractStringByPointers(payload, RESPONSE_THREAD_ID_POINTERS); } +function extractHeaderThreadId(headers) { + if (!headers || typeof headers !== "object") { + return null; + } + const candidates = [ + "thread-id", + "x-client-request-id", + "conversation_id", + "conversation-id", + "session_id", + "session-id", + "x-codex-parent-thread-id", + ]; + for (const key of candidates) { + const raw = headers[key]; + if (Array.isArray(raw)) { + for (const item of raw) { + const text = firstNonEmptyString(item); + if (text) { + return text; + } + } + continue; + } + const text = firstNonEmptyString(raw); + if (text) { + return text; + } + } + return null; +} + function extractNonStreamingResponseId(payload) { return extractStringByPointers(payload, NON_STREAM_RESPONSE_ID_POINTERS); } @@ -1312,7 +1365,10 @@ function buildRequestEntry({ seq, startedAt, startedMs, req, pathname, requestJs seq, request_id: null, response_id: null, - thread_id: extractRequestThreadId(requestJson), + thread_id: firstNonEmptyString( + extractRequestThreadId(requestJson), + extractHeaderThreadId(req?.headers), + ), lifecycle_state: "sent", started_at: startedAt.toISOString(), first_response_at: null, @@ -3234,7 +3290,10 @@ async function proxyRequest(runtime, req, res) { let totalUpstreamAttempts = 0; requestEntry.request_body_bytes = rawRequestBody.length; requestEntry.request_id = computeRequestId(pathname, rawRequestBody); - requestEntry.thread_id = extractRequestThreadId(parsedRequestJson); + requestEntry.thread_id = firstNonEmptyString( + extractRequestThreadId(parsedRequestJson), + extractHeaderThreadId(req.headers), + ); requestEntry.model = requestJson?.model || null; requestEntry.requested_model = parsedRequestJson?.model || null; requestEntry.forwarded_model = forwardedModel || parsedRequestJson?.model || null; diff --git a/scripts/test-gateway-e2e.mjs b/scripts/test-gateway-e2e.mjs index f8be654..a84136b 100644 --- a/scripts/test-gateway-e2e.mjs +++ b/scripts/test-gateway-e2e.mjs @@ -700,6 +700,28 @@ async function run() { assert(streamThreadEntry?.response_id === "resp_stream", "stream 请求记录未保留 response_id"); assert(streamThreadEntry?.thread_id === "thread_stream_ok", "stream 请求记录未保留 thread_id"); + const metadataThreadResponse = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { + method: "POST", + headers: { + "content-type": "application/json", + "thread-id": "thread_header_fallback", + "x-client-request-id": "thread_header_request_id", + }, + body: JSON.stringify({ + test_reasoning_tokens: 128, + client_metadata: { + thread_id: "thread_client_metadata", + "x-codex-thread-id": "thread_client_metadata_alias", + }, + }), + }); + assert(metadataThreadResponse.status === 200, `metadata thread 请求失败: ${metadataThreadResponse.status}`); + await metadataThreadResponse.json(); + const metadataThreadRequestsResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/requests?query=${encodeURIComponent("thread_client_metadata")}`); + const metadataThreadRequestsPayload = await metadataThreadRequestsResponse.json(); + const metadataThreadEntry = (metadataThreadRequestsPayload?.entries || []).find((entry) => entry.thread_id === "thread_client_metadata"); + assert(metadataThreadEntry?.thread_id === "thread_client_metadata", "client_metadata.thread_id 未写入请求记录"); + const streamDisconnectedRetryResponse = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" },