[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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user