[codex] fix responses lifecycle parity and codex session serialization (#278)

* fix responses lifecycle parity

* remove websocket test debug logging

* fix responses proxy stream typecheck

* update compact upstream response expectations

* add community and deepwiki badges

* update linuxdo banner link

* fix responses review follow-ups

* preserve whitespace in responses terminal deltas

* fix responses stream reconciliation edges

* fix responses lifecycle parity and codex session serialization

* fix review follow-up edge cases

---------

Co-authored-by: apple <apple@appledeMacBook-Pro.local>
This commit is contained in:
Cita
2026-03-25 18:56:36 +08:00
committed by GitHub
parent 9ba4369a83
commit 639c387532
13 changed files with 777 additions and 67 deletions
@@ -0,0 +1,30 @@
const codexHttpSessionQueues = new Map<string, Promise<void>>();
export async function runCodexHttpSessionTask<T>(
sessionId: string,
task: () => Promise<T>,
): Promise<T> {
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
return task();
}
const previous = codexHttpSessionQueues.get(normalizedSessionId) || Promise.resolve();
const run = previous
.catch(() => undefined)
.then(() => task());
const tail = run.then(() => undefined, () => undefined);
codexHttpSessionQueues.set(normalizedSessionId, tail);
try {
return await run;
} finally {
if (codexHttpSessionQueues.get(normalizedSessionId) === tail) {
codexHttpSessionQueues.delete(normalizedSessionId);
}
}
}
export function resetCodexHttpSessionQueue(): void {
codexHttpSessionQueues.clear();
}
@@ -40,6 +40,7 @@ import {
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
import { summarizeConversationFileInputsInOpenAiBody } from '../capabilities/conversationFileCapabilities.js';
import { readRuntimeResponseText } from '../executors/types.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { canRetryProxyChannel, getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import {
@@ -394,7 +395,7 @@ export async function handleChatSurfaceRequest(
});
let rawText = '';
if (!upstreamContentType.includes('text/event-stream')) {
const fallbackText = await upstream.text();
const fallbackText = await readRuntimeResponseText(upstream);
rawText = fallbackText;
if (looksLikeResponsesSseText(fallbackText)) {
startSseResponse();
@@ -578,7 +579,7 @@ export async function handleChatSurfaceRequest(
rawText = collected.rawText;
upstreamData = collected.payload;
} else {
rawText = await upstream.text();
rawText = await readRuntimeResponseText(upstream);
if (looksLikeResponsesSseText(rawText)) {
upstreamData = collectResponsesFinalPayloadFromSseText(rawText, modelName).payload;
} else {
@@ -33,6 +33,8 @@ import {
unwrapGeminiCliPayload,
} from '../../routes/proxy/geminiCliCompat.js';
import { isCodexResponsesSurface } from '../cliProfiles/codexProfile.js';
import { readRuntimeResponseText } from '../executors/types.js';
import { runCodexHttpSessionTask } from '../runtime/codexHttpSessionQueue.js';
import {
summarizeConversationFileInputsInOpenAiBody,
summarizeConversationFileInputsInResponsesBody,
@@ -56,6 +58,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function getCodexSessionHeaderValue(headers: Record<string, string>): string {
for (const [rawKey, rawValue] of Object.entries(headers)) {
const normalizedKey = rawKey.trim().toLowerCase();
if (normalizedKey === 'session_id' || normalizedKey === 'session-id') {
return String(rawValue || '').trim();
}
}
return '';
}
function isResponsesWebsocketTransportRequest(headers: Record<string, unknown>): boolean {
return Object.entries(headers)
.some(([rawKey, rawValue]) => rawKey.trim().toLowerCase() === 'x-metapi-responses-websocket-transport'
@@ -324,10 +335,23 @@ export async function handleOpenAiResponsesSurfaceRequest(
runtime: endpointRequest.runtime,
};
};
const dispatchRequest = createSurfaceDispatchRequest({
const baseDispatchRequest = createSurfaceDispatchRequest({
site: selected.site,
accountExtraConfig: selected.account.extraConfig,
});
const dispatchRequest = (
endpointRequest: BuiltEndpointRequest,
targetUrl?: string,
) => {
if (!isCodexSite || endpointRequest.path !== '/responses') {
return baseDispatchRequest(endpointRequest, targetUrl);
}
const sessionId = getCodexSessionHeaderValue(endpointRequest.headers);
return runCodexHttpSessionTask(
sessionId,
() => baseDispatchRequest(endpointRequest, targetUrl),
);
};
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
isStream: isStream || isCodexSite,
requiresNativeResponsesFileUrl,
@@ -507,7 +531,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
},
});
if (!upstreamContentType.includes('text/event-stream')) {
const rawText = await upstream.text();
const rawText = await readRuntimeResponseText(upstream);
if (looksLikeResponsesSseText(rawText)) {
startSseResponse();
const streamResult = await streamSession.run(
@@ -614,7 +638,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
let replayReader: ReturnType<typeof createSingleChunkStreamReader> | null = null;
if (websocketTransportRequest) {
const rawText = await upstream.text();
const rawText = await readRuntimeResponseText(upstream);
if (looksLikeResponsesSseText(rawText)) {
try {
const collectedPayload = collectResponsesFinalPayloadFromSseText(rawText, modelName).payload;
@@ -745,7 +769,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
rawText = collected.rawText;
upstreamData = collected.payload;
} else {
rawText = await upstream.text();
rawText = await readRuntimeResponseText(upstream);
if (looksLikeResponsesSseText(rawText)) {
upstreamData = collectResponsesFinalPayloadFromSseText(rawText, modelName).payload;
} else {