Summary
When shadow call intercept is enabled (shadowCallIntercept.enabled: true) and targets a combo (e.g. shadowCallIntercept.model: "combo/shadow"), background helper calls such as gpt-5.6-luna (used by Codex Desktop for thread title generation, summarization, etc.) are rewritten, but only execute a single attempt against the combo's first candidate.
If the primary candidate fails with HTTP 429 (rate limit / quota exhausted) or HTTP 5xx, the proxy immediately returns the error to the client with a single attempt recorded, completely skipping the failover loop to subsequent combo candidates.
Root Cause
In src/server/responses/core.ts, the request routing and dispatch pipeline executes in the wrong order relative to combo handling:
- Combo dispatch gate is early (lines ~3173-3175):
const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
options.onRequestBodyRead?.();
return handleComboResponses(req, body, comboId, config, logCtx, ...);
}
At this point, the incoming helper request has body.model = "gpt-5.6-luna", so comboIdFromRawBody returns null. The request skips handleComboResponses (which hosts the while (pick) loop and failover / advanceComboAfterFailure logic).
- Shadow call intercept runs later (lines ~3345-3375):
const _sci = config.shadowCallIntercept;
if (_sci?.enabled && _sci.model && isShadowSourceModel(parsed.modelId, _sci.sourceModels)) {
...
const targetRoute = resolveRoute(_sci.model);
if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
parsed.modelId = _sci.model;
logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
shadowSourceModelPrefix(_sciOriginal, _sci.sourceModels),
);
shadowRoute = targetRoute;
}
}
route = shadowRoute ?? resolveRoute(parsed.modelId);
Here, resolveRoute("combo/shadow") calls tryPickComboModel, which selects only a single concrete candidate (e.g. sensenova/deepseek-v4-flash) and returns a route tagged routeKind: "combo".
- Single attempt executed without retry loop:
The request proceeds down the normal dispatch path and sends a single request to the selected upstream provider. When upstream returns 429, because the execution is not inside handleComboResponses, there is no outer loop to catch the failure and advance to the next target. The 429 error is directly returned to the client.
Reproduction
- Configure a failover combo in
config.json:
"combos": {
"shadow": {
"strategy": "failover",
"targets": [
{ "provider": "provider-a", "model": "model-a", "weight": 1 },
{ "provider": "provider-b", "model": "model-b", "weight": 1 }
]
}
}
- Configure shadow call interception to target the combo:
"shadowCallIntercept": {
"enabled": true,
"model": "combo/shadow",
"sourceModels": ["gpt-5.6-luna"]
}
- Trigger a rate limit / 429 on
provider-a/model-a.
- Open a new thread in Codex Desktop and send a message (triggering
gpt-5.6-luna title generation in background).
- Inspect request in
/api/request-history/<id>:
routeDecision.routeKind is "combo"
attempts contains only 1 attempt
- Response is 429,
provider-b/model-b is never tried.
Suggested Fix
Move the shadow call model rewrite before the combo dispatch check (comboIdFromRawBody at line 3173) so that when parsed.modelId is rewritten to combo/..., it is recognized by comboIdFromRawBody and routed into handleComboResponses, enabling the full multi-target failover loop.
Summary
When shadow call intercept is enabled (
shadowCallIntercept.enabled: true) and targets a combo (e.g.shadowCallIntercept.model: "combo/shadow"), background helper calls such asgpt-5.6-luna(used by Codex Desktop for thread title generation, summarization, etc.) are rewritten, but only execute a single attempt against the combo's first candidate.If the primary candidate fails with HTTP 429 (rate limit / quota exhausted) or HTTP 5xx, the proxy immediately returns the error to the client with a single attempt recorded, completely skipping the failover loop to subsequent combo candidates.
Root Cause
In
src/server/responses/core.ts, the request routing and dispatch pipeline executes in the wrong order relative to combo handling:At this point, the incoming helper request has
body.model = "gpt-5.6-luna", socomboIdFromRawBodyreturnsnull. The request skipshandleComboResponses(which hosts thewhile (pick)loop and failover /advanceComboAfterFailurelogic).Here,
resolveRoute("combo/shadow")callstryPickComboModel, which selects only a single concrete candidate (e.g.sensenova/deepseek-v4-flash) and returns a route taggedrouteKind: "combo".The request proceeds down the normal dispatch path and sends a single request to the selected upstream provider. When upstream returns 429, because the execution is not inside
handleComboResponses, there is no outer loop to catch the failure and advance to the next target. The 429 error is directly returned to the client.Reproduction
config.json:provider-a/model-a.gpt-5.6-lunatitle generation in background)./api/request-history/<id>:routeDecision.routeKindis"combo"attemptscontains only 1 attemptprovider-b/model-bis never tried.Suggested Fix
Move the shadow call model rewrite before the combo dispatch check (
comboIdFromRawBodyat line 3173) so that whenparsed.modelIdis rewritten tocombo/..., it is recognized bycomboIdFromRawBodyand routed intohandleComboResponses, enabling the full multi-target failover loop.