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