Fix responses compact fallback handling (#426)

* fix route drag overlay alignment

* fix(web): address route drag review comments

* fix route card review follow-up

* Fix responses compact fallback handling

* Fix compact endpoint candidate typing

* Tighten compact fallback matcher
This commit is contained in:
Cita
2026-04-05 19:41:09 +08:00
committed by GitHub
parent b3f6fa0d4f
commit df75a80bf9
10 changed files with 368 additions and 29 deletions
@@ -0,0 +1,40 @@
function asTrimmedString(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
export function sanitizeCompactResponsesRequestBody(
body: Record<string, unknown>,
): Record<string, unknown> {
const next = { ...body };
delete next.stream;
delete next.stream_options;
return next;
}
export function shouldFallbackCompactResponsesToResponses(input: {
status?: number;
rawErrText?: string;
}): boolean {
const status = Number.isFinite(Number(input.status)) ? Number(input.status) : 0;
const compact = asTrimmedString(input.rawErrText).toLowerCase();
const hasCompactHint = (
compact.includes('/responses/compact')
|| compact.includes('responses/compact')
|| compact.includes('compact endpoint')
|| /(^|[^a-z])compact([^a-z]|$)/.test(compact)
);
if (status === 404 || status === 405 || status === 501) return true;
return (
compact.includes("unknown parameter: 'stream'")
|| compact.includes('invalid url')
|| (
hasCompactHint
&& (
compact.includes('not supported')
|| compact.includes('unsupported')
)
)
);
}
@@ -14,6 +14,7 @@ import {
import {
buildUpstreamEndpointRequest,
resolveUpstreamEndpointCandidates,
type UpstreamEndpoint,
} from '../../services/upstreamEndpointRuntime.js';
import {
getUpstreamEndpointRuntimeStateSnapshot,
@@ -57,6 +58,10 @@ import {
summarizeConversationFileInputsInOpenAiBody,
summarizeConversationFileInputsInResponsesBody,
} from '../capabilities/conversationFileCapabilities.js';
import {
sanitizeCompactResponsesRequestBody,
shouldFallbackCompactResponsesToResponses,
} from '../capabilities/responsesCompact.js';
import { detectDownstreamClientContext } from '../../routes/proxy/downstreamClientContext.js';
import { getProxyMaxChannelRetries } from '../../services/proxyChannelRetry.js';
import { shouldAbortSameSiteEndpointFallback } from '../../services/proxyRetryPolicy.js';
@@ -411,20 +416,22 @@ export async function handleOpenAiResponsesSurfaceRequest(
const responsesConversationFileSummary = summarizeConversationFileInputsInResponsesBody(normalizedResponsesBody);
const requiresNativeResponsesFileUrl = responsesConversationFileSummary.hasRemoteDocumentUrl
|| carriesResponsesFileUrlInput(normalizedResponsesBody.input);
const endpointCandidates = await resolveUpstreamEndpointCandidates(
{
site: selected.site,
account: selected.account,
},
modelName,
'responses',
requestedModel,
{
hasNonImageFileInput,
conversationFileSummary,
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
},
);
const endpointCandidates: UpstreamEndpoint[] = isCompactRequest
? ['responses']
: await resolveUpstreamEndpointCandidates(
{
site: selected.site,
account: selected.account,
},
modelName,
'responses',
requestedModel,
{
hasNonImageFileInput,
conversationFileSummary,
wantsNativeResponsesReasoning: prefersNativeResponsesReasoning,
},
);
if (endpointCandidates.length === 0) {
endpointCandidates.push('responses', 'chat', 'messages');
}
@@ -460,8 +467,9 @@ export async function handleOpenAiResponsesSurfaceRequest(
})
);
const executeEndpointResultForSiteApiBaseUrl = async (siteApiBaseUrl: string) => {
const forceCodexUpstreamStream = isCodexSite && !isCompactRequest;
const buildEndpointRequest = (endpoint: 'chat' | 'messages' | 'responses') => {
const upstreamStream = isStream || (isCodexSite && endpoint === 'responses');
const upstreamStream = isStream || (forceCodexUpstreamStream && endpoint === 'responses');
const responsesOriginalBody = (
endpoint === 'responses'
&& isCodexSite
@@ -496,11 +504,16 @@ export async function handleOpenAiResponsesSurfaceRequest(
? `${endpointRequest.path}/compact`
: endpointRequest.path
);
const requestBody = (
isCompactRequest && endpoint === 'responses'
? sanitizeCompactResponsesRequestBody(endpointRequest.body as Record<string, unknown>)
: endpointRequest.body as Record<string, unknown>
);
return {
endpoint,
path: upstreamPath,
headers: endpointRequest.headers,
body: endpointRequest.body as Record<string, unknown>,
body: requestBody,
runtime: endpointRequest.runtime,
};
};
@@ -523,7 +536,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
);
};
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
isStream: isStream || isCodexSite,
isStream: isStream || forceCodexUpstreamStream,
requiresNativeResponsesFileUrl,
dispatchRequest,
});
@@ -574,13 +587,39 @@ export async function handleOpenAiResponsesSurfaceRequest(
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
}
}
if (
isCompactRequest
&& config.responsesCompactFallbackToResponsesEnabled
&& ctx.request.endpoint === 'responses'
&& ctx.request.path.endsWith('/responses/compact')
&& shouldFallbackCompactResponsesToResponses({
status: ctx.response.status,
rawErrText: ctx.rawErrText,
})
) {
const recoveredRequest = {
...ctx.request,
path: ctx.request.path.replace(/\/compact$/, ''),
};
const recoveredResponse = await dispatchRequest(recoveredRequest);
if (recoveredResponse.ok) {
return {
upstream: recoveredResponse,
upstreamPath: recoveredRequest.path,
request: recoveredRequest,
};
}
ctx.request = recoveredRequest;
ctx.response = recoveredResponse;
ctx.rawErrText = await readRuntimeResponseText(recoveredResponse).catch(() => 'unknown error');
}
return endpointStrategy.tryRecover(ctx);
};
const debugAttemptBase = reserveSurfaceProxyDebugAttemptBase(debugTrace, endpointCandidates.length);
return executeEndpointFlow({
siteUrl: siteApiBaseUrl,
disableCrossProtocolFallback: config.disableCrossProtocolFallback,
disableCrossProtocolFallback: isCompactRequest || config.disableCrossProtocolFallback,
firstByteTimeoutMs: Math.max(0, Math.trunc((config.proxyFirstByteTimeoutSec || 0) * 1000)),
endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint),
@@ -591,12 +630,14 @@ export async function handleOpenAiResponsesSurfaceRequest(
ctx.rawErrText || ctx.errText,
),
onAttemptFailure: async (ctx) => {
const memoryWrite = recordUpstreamEndpointFailure({
...endpointRuntimeContext,
endpoint: ctx.request.endpoint,
status: ctx.response.status,
errorText: ctx.rawErrText,
});
const memoryWrite = isCompactRequest
? null
: recordUpstreamEndpointFailure({
...endpointRuntimeContext,
endpoint: ctx.request.endpoint,
status: ctx.response.status,
errorText: ctx.rawErrText,
});
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
attemptIndex: debugAttemptBase + ctx.endpointIndex,
endpoint: ctx.request.endpoint,
@@ -616,10 +657,12 @@ export async function handleOpenAiResponsesSurfaceRequest(
});
},
onAttemptSuccess: async (ctx) => {
const memoryWrite = recordUpstreamEndpointSuccess({
...endpointRuntimeContext,
endpoint: ctx.request.endpoint,
});
const memoryWrite = isCompactRequest
? null
: recordUpstreamEndpointSuccess({
...endpointRuntimeContext,
endpoint: ctx.request.endpoint,
});
const responseBody = await captureSurfaceProxyDebugSuccessResponseBody(debugTrace, ctx);
await safeInsertSurfaceProxyDebugAttempt(debugTrace, {
attemptIndex: debugAttemptBase + ctx.endpointIndex,