Skip to content

[Bug]: shadowCallIntercept targeting a combo does not trigger handleComboResponses failover loop #4129

Description

@jaychou0642-create

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:

  1. 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).

  1. 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".

  1. 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

  1. 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 }
    ]
  }
}
  1. Configure shadow call interception to target the combo:
"shadowCallIntercept": {
  "enabled": true,
  "model": "combo/shadow",
  "sourceModels": ["gpt-5.6-luna"]
}
  1. Trigger a rate limit / 429 on provider-a/model-a.
  2. Open a new thread in Codex Desktop and send a message (triggering gpt-5.6-luna title generation in background).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions