feat: enhance error handling for semaphore capacity and implement fallback logic in chat processing

This commit is contained in:
Jan Leon
2026-05-08 21:29:13 +00:00
parent 640ed6d2bc
commit 32f3f3d94f
2 changed files with 55 additions and 7 deletions

View File

@@ -595,14 +595,39 @@ function toFiniteNumberOrNull(value: unknown): number | null {
return null;
}
function isSemaphoreTimeoutError(error: unknown): error is Error & { code: string } {
function isSemaphoreCapacityError(error: unknown): error is Error & { code: string } {
return (
!!error &&
typeof error === "object" &&
(error as { code?: unknown }).code === "SEMAPHORE_TIMEOUT"
((error as { code?: unknown }).code === "SEMAPHORE_TIMEOUT" ||
(error as { code?: unknown }).code === "SEMAPHORE_QUEUE_FULL")
);
}
function createStreamingErrorResult(statusCode: number, message: string, code?: string) {
const errorBody = buildErrorBody(statusCode, message);
if (code) {
errorBody.error.code = code;
}
const body = `data: ${JSON.stringify(errorBody)}\n\ndata: [DONE]\n\n`;
return {
success: false as const,
status: statusCode,
error: message,
response: new Response(body, {
status: statusCode,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
}),
};
}
function wrapReadableStreamWithFinalize<T>(
readable: ReadableStream<T>,
finalize: () => void
@@ -2998,16 +3023,13 @@ export async function handleChatCore({
);
} catch (error) {
trackPendingRequest(model, provider, connectionId, false);
if (isSemaphoreTimeoutError(error)) {
if (isSemaphoreCapacityError(error)) {
appendRequestLog({
model,
provider,
connectionId,
status: `FAILED ${error.code}`,
}).catch(() => {});
if (isCombo) {
throw error;
}
const failureMessage = error.message || "Semaphore timeout";
persistAttemptLogs({
status: HTTP_STATUS.RATE_LIMITED,
@@ -3018,7 +3040,14 @@ export async function handleChatCore({
cacheSource: "upstream",
});
persistFailureUsage(HTTP_STATUS.RATE_LIMITED, error.code);
return createErrorResult(HTTP_STATUS.RATE_LIMITED, failureMessage);
const result = stream
? createStreamingErrorResult(HTTP_STATUS.RATE_LIMITED, failureMessage, error.code)
: createErrorResult(HTTP_STATUS.RATE_LIMITED, failureMessage);
return {
...result,
errorType: "account_semaphore_capacity",
errorCode: error.code,
};
}
const failureStatus =
error.name === "AbortError"

View File

@@ -791,6 +791,25 @@ async function handleSingleModelChat(
return result.response;
}
if (result.errorType === "account_semaphore_capacity") {
// Local concurrency pressure is not an upstream quota failure. Prefer another
// account when possible; pinned combo steps fall through to combo orchestration.
if (hasForcedConnection) {
return result.response;
}
log.warn(
"AUTH",
`Account ${accountId}... at local concurrency cap, trying fallback account`
);
excludedConnectionIds.add(credentials.connectionId);
lastError = result.error;
lastStatus = result.status;
requestRetryLastError = result.error;
requestRetryLastStatus = result.status;
continue;
}
// Emergency fallback for budget exhaustion (402 / billing / quota keywords):
// reroute to a free model (default provider/model: nvidia + openai/gpt-oss-120b) exactly once.
if (!runtimeOptions.emergencyFallbackTried) {