From 90f0c308615a741b6ffe0adcd642d7836295535d Mon Sep 17 00:00:00 2001 From: ciat-777 Date: Wed, 18 Mar 2026 13:25:42 +0800 Subject: [PATCH] fix: align codex responses continuity handling --- .../proxy/responses.codex-oauth.test.ts | 41 ++++++++++++--- src/server/routes/proxy/responses.ts | 29 +++-------- .../routes/proxy/upstreamEndpoint.test.ts | 51 +++++++++++++++++++ src/server/routes/proxy/upstreamEndpoint.ts | 23 +++++++-- 4 files changed, 112 insertions(+), 32 deletions(-) diff --git a/src/server/routes/proxy/responses.codex-oauth.test.ts b/src/server/routes/proxy/responses.codex-oauth.test.ts index c73a50e..e3cc2f8 100644 --- a/src/server/routes/proxy/responses.codex-oauth.test.ts +++ b/src/server/routes/proxy/responses.codex-oauth.test.ts @@ -193,9 +193,7 @@ describe('responses proxy codex oauth refresh', () => { expect(secondOptions.headers['Chatgpt-Account-Id'] || secondOptions.headers['chatgpt-account-id']).toBe('chatgpt-account-123'); expect(secondOptions.headers.Version || secondOptions.headers.version).toBe('0.101.0'); expect(String(secondOptions.headers.Session_id || secondOptions.headers.session_id || '')).toMatch(/^[0-9a-f-]{36}$/i); - expect(secondOptions.headers.Conversation_id || secondOptions.headers.conversation_id).toBe( - secondOptions.headers.Session_id || secondOptions.headers.session_id, - ); + expect(secondOptions.headers.Conversation_id || secondOptions.headers.conversation_id).toBeUndefined(); expect(secondOptions.headers['User-Agent'] || secondOptions.headers['user-agent']).toBe('codex_cli_rs/0.101.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464'); expect(secondOptions.headers.Accept || secondOptions.headers.accept).toBe('text/event-stream'); expect(secondOptions.headers.Connection || secondOptions.headers.connection).toBe('Keep-Alive'); @@ -230,9 +228,7 @@ describe('responses proxy codex oauth refresh', () => { const [, options] = fetchMock.mock.calls[0] as [string, any]; const forwardedBody = JSON.parse(options.body); expect(forwardedBody.instructions).toBe(''); - expect(forwardedBody.prompt_cache_key).toBe( - options.headers.Session_id || options.headers.session_id, - ); + expect(forwardedBody.prompt_cache_key).toBeUndefined(); expect(forwardedBody.stream).toBe(true); expect(forwardedBody.input).toEqual([ { @@ -243,6 +239,39 @@ describe('responses proxy codex oauth refresh', () => { ]); }); + it('preserves explicit prompt_cache_key for codex responses requests', async () => { + fetchMock.mockResolvedValue(new Response(JSON.stringify({ + id: 'resp_codex_with_cache_key', + object: 'response', + model: 'gpt-5.2-codex', + status: 'completed', + output_text: 'ok with cache key', + usage: { input_tokens: 4, output_tokens: 2, total_tokens: 6 }, + }), { + status: 200, + headers: { 'content-type': 'application/json' }, + })); + + const response = await app.inject({ + method: 'POST', + url: '/v1/responses', + payload: { + model: 'gpt-5.2-codex', + prompt_cache_key: 'codex-cache-123', + input: 'hello codex', + }, + }); + + expect(response.statusCode).toBe(200); + expect(fetchMock).toHaveBeenCalledTimes(1); + + const [, options] = fetchMock.mock.calls[0] as [string, any]; + const forwardedBody = JSON.parse(options.body); + expect(options.headers.Session_id || options.headers.session_id).toBe('codex-cache-123'); + expect(options.headers.Conversation_id || options.headers.conversation_id).toBe('codex-cache-123'); + expect(forwardedBody.prompt_cache_key).toBe('codex-cache-123'); + }); + it('records codex usage_limit_reached reset hints on upstream 429 failures', async () => { fetchMock.mockResolvedValue(new Response(JSON.stringify({ error: { diff --git a/src/server/routes/proxy/responses.ts b/src/server/routes/proxy/responses.ts index 1eea1f4..fe73c68 100644 --- a/src/server/routes/proxy/responses.ts +++ b/src/server/routes/proxy/responses.ts @@ -124,24 +124,11 @@ function carriesResponsesFileUrlInput(value: unknown): boolean { type UsageSummary = ReturnType; -function deriveCodexSessionCacheKey(input: { - body: Record; - requestedModel: string; - proxyToken: string | null; -}): string | null { - const promptCacheKey = typeof input.body.prompt_cache_key === 'string' - ? input.body.prompt_cache_key.trim() +function deriveCodexExplicitSessionId(body: Record): string | null { + const promptCacheKey = typeof body.prompt_cache_key === 'string' + ? body.prompt_cache_key.trim() : ''; - if (promptCacheKey) { - return `${input.requestedModel}:responses:${promptCacheKey}`; - } - - const proxyToken = typeof input.proxyToken === 'string' ? input.proxyToken.trim() : ''; - if (proxyToken) { - return `${input.requestedModel}:proxy:${proxyToken}`; - } - - return null; + return promptCacheKey || null; } export async function responsesProxyRoute(app: FastifyInstance) { @@ -271,11 +258,7 @@ export async function responsesProxyRoute(app: FastifyInstance) { ); const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => { const upstreamStream = isStream || (isCodexSite && endpoint === 'responses'); - const codexSessionCacheKey = deriveCodexSessionCacheKey({ - body: normalizedResponsesBody, - requestedModel, - proxyToken: getProxyAuthContext(request)?.token || null, - }); + const codexExplicitSessionId = deriveCodexExplicitSessionId(normalizedResponsesBody); const endpointRequest = buildUpstreamEndpointRequest({ endpoint, modelName, @@ -290,7 +273,7 @@ export async function responsesProxyRoute(app: FastifyInstance) { responsesOriginalBody: normalizedResponsesBody, downstreamHeaders: request.headers as Record, providerHeaders: buildProviderHeaders(), - codexSessionCacheKey, + codexExplicitSessionId, }); const upstreamPath = ( isCompactRequest && endpoint === 'responses' diff --git a/src/server/routes/proxy/upstreamEndpoint.test.ts b/src/server/routes/proxy/upstreamEndpoint.test.ts index 2f0bf0c..6b22a91 100644 --- a/src/server/routes/proxy/upstreamEndpoint.test.ts +++ b/src/server/routes/proxy/upstreamEndpoint.test.ts @@ -350,6 +350,7 @@ describe('buildUpstreamEndpointRequest', () => { Originator: 'codex_cli_rs', 'Chatgpt-Account-Id': 'chatgpt-account-123', }, + codexSessionCacheKey: 'gpt-5.2-codex:proxy:test-key', } as any); expect(request.path).toBe('/responses'); @@ -417,6 +418,56 @@ describe('buildUpstreamEndpointRequest', () => { expect(firstRequest.body.prompt_cache_key).toBe(secondRequest.body.prompt_cache_key); }); + it('does not synthesize prompt_cache_key or conversation_id for native codex responses requests without one', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'responses', + modelName: 'gpt-5.4', + stream: false, + tokenValue: 'oauth-access-token', + sitePlatform: 'codex', + siteUrl: 'https://chatgpt.com/backend-api/codex', + openaiBody: {}, + downstreamFormat: 'responses', + responsesOriginalBody: { + model: 'gpt-5.4', + input: 'hello codex', + }, + providerHeaders: { + Originator: 'codex_cli_rs', + }, + } as any); + + expect(request.headers.Session_id).toMatch(/^[0-9a-f-]{36}$/i); + expect(request.headers.Conversation_id).toBeUndefined(); + expect(request.body.prompt_cache_key).toBeUndefined(); + }); + + it('preserves explicit prompt_cache_key for native codex responses requests', () => { + const request = buildUpstreamEndpointRequest({ + endpoint: 'responses', + modelName: 'gpt-5.4', + stream: false, + tokenValue: 'oauth-access-token', + sitePlatform: 'codex', + siteUrl: 'https://chatgpt.com/backend-api/codex', + openaiBody: {}, + downstreamFormat: 'responses', + responsesOriginalBody: { + model: 'gpt-5.4', + prompt_cache_key: 'codex-cache-123', + input: 'hello codex', + }, + providerHeaders: { + Originator: 'codex_cli_rs', + }, + codexExplicitSessionId: 'codex-cache-123', + } as any); + + expect(request.headers.Session_id).toBe('codex-cache-123'); + expect(request.headers.Conversation_id).toBe('codex-cache-123'); + expect(request.body.prompt_cache_key).toBe('codex-cache-123'); + }); + it('builds gemini-cli native requests with project envelope and bearer headers', () => { const request = buildUpstreamEndpointRequest({ endpoint: 'responses', diff --git a/src/server/routes/proxy/upstreamEndpoint.ts b/src/server/routes/proxy/upstreamEndpoint.ts index 8ab6bba..d9f4715 100644 --- a/src/server/routes/proxy/upstreamEndpoint.ts +++ b/src/server/routes/proxy/upstreamEndpoint.ts @@ -283,16 +283,26 @@ function extractClaudeBetasFromBody(body: Record): { function buildCodexRuntimeHeaders(input: { baseHeaders: Record; providerHeaders?: Record; + explicitSessionId?: string | null; continuityKey?: string | null; }): Record { const originator = getInputHeader(input.providerHeaders, 'originator') || 'codex_cli_rs'; const accountId = getInputHeader(input.providerHeaders, 'chatgpt-account-id'); + const explicitSessionId = asTrimmedString(input.explicitSessionId); + const continuityKey = asTrimmedString(input.continuityKey); const sessionId = ( getInputHeader(input.baseHeaders, 'session_id') || getInputHeader(input.baseHeaders, 'session-id') - || (input.continuityKey ? uuidFromSeed(`metapi:codex:${input.continuityKey}`) : null) + || explicitSessionId + || (continuityKey ? uuidFromSeed(`metapi:codex:${continuityKey}`) : null) || randomUUID() ); + const conversationId = ( + getInputHeader(input.baseHeaders, 'conversation_id') + || getInputHeader(input.baseHeaders, 'conversation-id') + || explicitSessionId + || (continuityKey ? sessionId : null) + ); return { ...input.baseHeaders, @@ -300,7 +310,7 @@ function buildCodexRuntimeHeaders(input: { Originator: originator, Version: CODEX_CLIENT_VERSION, Session_id: sessionId, - Conversation_id: sessionId, + ...(conversationId ? { Conversation_id: conversationId } : {}), 'User-Agent': CODEX_DEFAULT_USER_AGENT, Accept: 'text/event-stream', Connection: 'Keep-Alive', @@ -753,6 +763,7 @@ export function buildUpstreamEndpointRequest(input: { downstreamHeaders?: Record; providerHeaders?: Record; codexSessionCacheKey?: string | null; + codexExplicitSessionId?: string | null; }): { path: string; headers: Record; @@ -985,6 +996,7 @@ export function buildUpstreamEndpointRequest(input: { ...responsesHeaders, }, providerHeaders: input.providerHeaders, + explicitSessionId: asTrimmedString(input.codexExplicitSessionId) || null, continuityKey: asTrimmedString(input.codexSessionCacheKey) || null, }) : ensureStreamAcceptHeader({ @@ -994,7 +1006,12 @@ export function buildUpstreamEndpointRequest(input: { const codexSessionId = sitePlatform === 'codex' ? (getInputHeader(headers, 'session_id') || getInputHeader(headers, 'session-id')) : null; - const runtimeBody = sitePlatform === 'codex' && codexSessionId + const shouldInjectDerivedPromptCacheKey = sitePlatform === 'codex' + && !!codexSessionId + && !asTrimmedString((body as Record).prompt_cache_key) + && !asTrimmedString(input.codexExplicitSessionId) + && !!asTrimmedString(input.codexSessionCacheKey); + const runtimeBody = shouldInjectDerivedPromptCacheKey ? { ...body, prompt_cache_key: codexSessionId,