forked from boos/BoosAPI
fix: align codex responses continuity handling
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -124,24 +124,11 @@ function carriesResponsesFileUrlInput(value: unknown): boolean {
|
||||
|
||||
type UsageSummary = ReturnType<typeof parseProxyUsage>;
|
||||
|
||||
function deriveCodexSessionCacheKey(input: {
|
||||
body: Record<string, unknown>;
|
||||
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, unknown>): 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<string, unknown>,
|
||||
providerHeaders: buildProviderHeaders(),
|
||||
codexSessionCacheKey,
|
||||
codexExplicitSessionId,
|
||||
});
|
||||
const upstreamPath = (
|
||||
isCompactRequest && endpoint === 'responses'
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -283,16 +283,26 @@ function extractClaudeBetasFromBody(body: Record<string, unknown>): {
|
||||
function buildCodexRuntimeHeaders(input: {
|
||||
baseHeaders: Record<string, string>;
|
||||
providerHeaders?: Record<string, string>;
|
||||
explicitSessionId?: string | null;
|
||||
continuityKey?: string | null;
|
||||
}): Record<string, string> {
|
||||
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<string, unknown>;
|
||||
providerHeaders?: Record<string, string>;
|
||||
codexSessionCacheKey?: string | null;
|
||||
codexExplicitSessionId?: string | null;
|
||||
}): {
|
||||
path: string;
|
||||
headers: Record<string, string>;
|
||||
@@ -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<string, unknown>).prompt_cache_key)
|
||||
&& !asTrimmedString(input.codexExplicitSessionId)
|
||||
&& !!asTrimmedString(input.codexSessionCacheKey);
|
||||
const runtimeBody = shouldInjectDerivedPromptCacheKey
|
||||
? {
|
||||
...body,
|
||||
prompt_cache_key: codexSessionId,
|
||||
|
||||
Reference in New Issue
Block a user