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 comboFailureDecision → advanceComboAfterFailure 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?
- 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.
- 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.
- Default for both: unset → current behaviour (inherit
connectTimeoutMs, no total budget), so existing configs are unchanged.
- 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
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 withocx 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?
connectTimeoutMs(default200000) 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 reachescomboFailureDecision→advanceComboAfterFailureand hops.OcxComboConfig(src/types/config.ts) has no per-attempt or per-request time budget: onlytargets,strategy,stickyLimit,defaultEffort,imageInput. I checkedmainon 2026-09-02 and this is still the case after v2.40.0.connectTimeoutMsglobally 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-flashcombo while the upstream was rate-limiting and stalling):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):
What should OpenCodex do?
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 (existingcoolComboTarget), log the[combo] … failed with … after …msline with a distinct reason (e.g.first-byte-timeout), and advance to the next eligible target.requestBudgetMs). When the sum of attempts would exceed it, return the last failure instead of starting another attempt.connectTimeoutMs, no total budget), so existing configs are unchanged.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
Expected behaviour with that config:
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
connectTimeoutMsglobally — rejected, it changes behaviour for every non-combo provider.src/server/responses/core.tslocally — works, but the patch is lost on everyocx update.Additional context
Checks