mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 12:52:25 +03:00
* fix(combo): retry pre-content streaming failures * fix(combo): allow same-target retry for native-pinned pre-content failures A native Codex turn pin forced maxRetries to 0 unconditionally, which also disabled same-target retries. A pre-content stream failure sends no bytes to the client, so retrying the same pinned target is safe and indistinguishable from a first attempt. Set retries stay disabled so a pinned turn can never fail over to a different target. Adds a regression test covering a pinned turn whose first attempt fails before any content is streamed. * docs(changelog): add fragment for pre-content streaming retry fix Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
/**
|
|
* Pure classify helpers for executeTarget's retry loop.
|
|
* Lift-as-is from combo.ts #8375 / #2101 / #4279. No I/O.
|
|
*
|
|
* @internal — not part of the public combo.ts barrel.
|
|
*/
|
|
import {
|
|
isContextOverflow400,
|
|
isInputBoundRequestFailure,
|
|
isModelScoped400,
|
|
isParamValidation400,
|
|
} from "./comboPredicates.ts";
|
|
import { errorResponse } from "../../utils/error.ts";
|
|
|
|
export function remainderIsHomogeneous(
|
|
orderedTargets: { modelStr: string }[],
|
|
index: number,
|
|
modelStr: string
|
|
): boolean {
|
|
return orderedTargets.slice(index + 1).every((nextInPool) => nextInPool.modelStr === modelStr);
|
|
}
|
|
|
|
/**
|
|
* Handle a pre-content streaming upstream error: nothing reached the client
|
|
* yet, so re-dispatching the same target cannot duplicate output. Logs and
|
|
* returns true when the caller should retry, false to fall through.
|
|
*/
|
|
export function handlePreContentStreamRetry(
|
|
quality: { reason?: string | null },
|
|
retry: number,
|
|
deps: {
|
|
maxRetries: number;
|
|
signal?: { aborted?: boolean } | null;
|
|
log: { info: (tag: string, msg: string) => void };
|
|
},
|
|
modelStr: string
|
|
): boolean {
|
|
if (
|
|
quality.reason !== "streaming upstream error" ||
|
|
retry >= deps.maxRetries ||
|
|
deps.signal?.aborted
|
|
) {
|
|
return false;
|
|
}
|
|
deps.log.info(
|
|
"COMBO",
|
|
`Retrying ${modelStr} after pre-content streaming upstream error ` +
|
|
`(attempt ${retry + 2}/${deps.maxRetries + 1})`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
/** Protected-priority target whose upstream body failed quality validation. */
|
|
export function qualityValidationFailure(): { ok: false; response: Response } {
|
|
return { ok: false, response: errorResponse(502, "Upstream response failed quality validation") };
|
|
}
|
|
|
|
export function shouldAbortOnInputBoundFailure(opts: {
|
|
structuredError: unknown;
|
|
remainderIsHomogeneous: boolean;
|
|
}): boolean {
|
|
const structured = opts.structuredError as
|
|
{ code?: string | null; type?: string | null } | undefined;
|
|
return isInputBoundRequestFailure(structured) && opts.remainderIsHomogeneous;
|
|
}
|
|
|
|
/**
|
|
* #2101 / #4279: body-specific 400 must surface via {ok,response}, not null.
|
|
* Same predicate chain as combo.ts (overflow / param / model-scoped excluded).
|
|
*/
|
|
export function shouldSurfaceBodySpecific400(opts: {
|
|
status: number;
|
|
errorText: string;
|
|
shouldFallback: boolean;
|
|
}): boolean {
|
|
const errorText = opts.errorText;
|
|
return (
|
|
opts.status === 400 &&
|
|
opts.shouldFallback &&
|
|
!isContextOverflow400(errorText) &&
|
|
!isParamValidation400(errorText) &&
|
|
!isModelScoped400(errorText) &&
|
|
(errorText.toLowerCase().includes("context") ||
|
|
errorText.toLowerCase().includes("prompt") ||
|
|
errorText.toLowerCase().includes("token") ||
|
|
errorText.toLowerCase().includes("malformed") ||
|
|
errorText.toLowerCase().includes("invalid") ||
|
|
errorText.toLowerCase().includes("bad request"))
|
|
);
|
|
}
|