Skip to content

feat(combo): per-combo attempt first-byte deadline so a stalled target hops before the global 200s connectTimeoutMs #3266

Description

@Veritas-7

Area

Proxy and routing

What are you trying to accomplish?

I run a combo (strategy: round-robin, three targets that point at the same upstream family through different keys) as a resilient lane for an agent client launched with ocx claude. When one target stalls (the upstream accepts the connection but never sends response headers), the request should hop to the next target within a bounded, combo-specific time so the agent's turn survives instead of timing out on the client side.

What prevents this today?

  • Every combo attempt inherits the global connectTimeoutMs (default 200000) as its header-arrival budget — src/server/chat-native.ts:171, src/server/claude-messages.ts:380, src/server/responses/core.ts:3820 (v2.38.0 line numbers). A stalled target therefore burns ~200 s before the combo loop reaches comboFailureDecisionadvanceComboAfterFailure and hops.
  • OcxComboConfig (src/types/config.ts) has no per-attempt or per-request time budget: only targets, strategy, stickyLimit, defaultEffort, imageInput. I checked main on 2026-09-02 and this is still the case after v2.40.0.
  • Lowering connectTimeoutMs globally is not a safe workaround: it also governs every non-combo provider, including ones whose first byte legitimately arrives late (long non-streaming reasoning).

Observed on one proxy over a 24 h window (v2.38.0, ~4.8k requests to a 3-target glm-5.3-flash combo while the upstream was rate-limiting and stalling):

metric value
requests where a target attempt failed with 502 only after ~200 s of header wait 11
of those, requests where the client cancelled (499) during the second attempt 6
wall time of those cancelled requests 343–359 s
healthy first-byte latency on the same proxy, same period (combo / openai / xai targets) p95 ≤ 24 s, max 53 s

So the 200 s wait is almost never a legitimate slow start for these targets; it is dead time that the 60 s target cooldown cannot recover because the cooldown only starts after the attempt fails.

Log shape (from the proxy console):

[combo] glm-round: glm-a/glm-5.3-flash failed with 502 after 200010ms
[combo] glm-round: glm-b/glm-5.3-flash failed with 500 after 180245ms
[combo] glm-round: glm-c/glm-5.3-flash failed with 502 after 200013ms

What should OpenCodex do?

  1. Accept an optional per-combo attempt first-byte deadline (name open, e.g. attemptFirstByteTimeoutMs). If a combo attempt has not received upstream response headers within that time, treat it exactly like a hop-able upstream failure: cool the target (existing coolComboTarget), log the [combo] … failed with … after …ms line with a distinct reason (e.g. first-byte-timeout), and advance to the next eligible target.
  2. Optionally accept a per-combo total request budget (e.g. requestBudgetMs). When the sum of attempts would exceed it, return the last failure instead of starting another attempt.
  3. Default for both: unset → current behaviour (inherit connectTimeoutMs, no total budget), so existing configs are unchanged.
  4. Expose both fields in the dashboard combo editor next to stickyLimit.

Scope note: this is not #486 (cooldown length; that issue was auto-closed for missing detail). Cooldown semantics stay as they are — this proposal only bounds the attempt itself, which the cooldown cannot do.

Example usage or interface

"combos": {
  "glm-round": {
    "strategy": "round-robin",
    "attemptFirstByteTimeoutMs": 60000,
    "requestBudgetMs": 180000,
    "targets": [
      { "provider": "glm-a", "model": "glm-5.3-flash" },
      { "provider": "glm-b", "model": "glm-5.3-flash" },
      { "provider": "glm-c", "model": "glm-5.3-flash" }
    ]
  }
}

Expected behaviour with that config:

[combo] glm-round: glm-a/glm-5.3-flash failed with first-byte-timeout after 60008ms
[combo] glm-round: glm-b/glm-5.3-flash ok after 4210ms

i.e. the hop happens after ~60 s instead of ~200 s, and the whole request can never exceed ~180 s of upstream time.

Alternatives or workarounds

  • Lower connectTimeoutMs globally — rejected, it changes behaviour for every non-combo provider.
  • Patch src/server/responses/core.ts locally — works, but the patch is lost on every ocx update.
  • Rely on the client's own turn timeout — the client does cancel (499), but only after the proxy has already spent 200 s+ on the stalled target, so the turn is lost either way.

Additional context

Checks

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

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

    enhancementNew feature or requestproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions