Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@

- **cline (empty/false-502 non-streaming responses):** the Cline gateway can wrap OpenAI-compatible chat completions in a `{ success, data: { choices, usage, … } }` envelope. The non-streaming path checked the top-level body for empty content before unwrapping, so a valid Cline response was treated as malformed. The body is now unwrapped via `unwrapClineNonStreamingEnvelope()` right after the provider-envelope unwrap and before the empty-content check, closing the remaining gap from #5956/#5924. Regression guard: `tests/unit/cline-response-envelope.test.ts`. ([#5956](https://github.com/diegosouzapw/OmniRoute/issues/5956) — thanks @KooshaPari)

- **combo (per-model-quota providers exhausted on a single model 500):** for per-model-quota providers (gemini, github, passthrough, compatible) that multiplex many models behind one connection, a model-level `500` (e.g. Gemini "Internal error encountered") wrongly marked the whole connection exhausted, so sibling models on the same connection were skipped and the combo could 502 instead of falling back. `markConnectionLevelExhaustion` now leaves the connection eligible on a `500` for per-model-quota providers (other connection-level statuses — 408/502/503/504/524 — still exhaust correctly), and the retry loop early-returns when a model is already in lockout. Regression guard: `tests/unit/combo/combo-target-exhaustion.test.ts`. ([#5976](https://github.com/diegosouzapw/OmniRoute/pull/5976) — thanks @hartmark)

### 📝 Maintenance

- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))
Expand Down
9 changes: 9 additions & 0 deletions open-sse/services/combo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,15 @@ export async function handleComboChat({
!isTokenLimitBreach &&
[408, 429, 500, 502, 503, 504].includes(result.status);
if (retry < maxRetries && isTransient && !providerExhausted) {
if (
provider &&
rawModel &&
isModelLocked(provider, targetWithConnection.connectionId || "", rawModel)
) {
log.info("COMBO", `Skipping retry for ${modelStr} — model lockout active`);
if (i > 0) fallbackCount++;
return null;
}
// Record model lockout immediately on the first transient failure —
// once the model is cooling down, retrying it would waste an upstream
// call and extend the cooldown via exponential backoff.
Expand Down
311 changes: 0 additions & 311 deletions open-sse/services/combo/__tests__/targetExhaustion.test.ts

This file was deleted.

17 changes: 13 additions & 4 deletions open-sse/services/combo/targetExhaustion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function applyComboTargetExhaustion(
if (result.status === 429 && !isTokenLimitBreach && provider && provider !== "unknown") {
transientRateLimitedProviders.add(provider);
}
markConnectionLevelExhaustion(target, { result, errorText, sets, log, tag });
markConnectionLevelExhaustion(target, { result, errorText, sets, log, tag, rawModel });
}

return providerExhausted;
Expand All @@ -118,9 +118,12 @@ export function applyComboTargetExhaustion(
*/
function markConnectionLevelExhaustion(
target: ResolvedComboTarget,
opts: Pick<ApplyComboTargetExhaustionOptions, "result" | "errorText" | "sets" | "log" | "tag">
opts: Pick<
ApplyComboTargetExhaustionOptions,
"result" | "errorText" | "sets" | "log" | "tag" | "rawModel"
>
): void {
const { result, errorText, sets, log, tag } = opts;
const { result, errorText, sets, log, tag, rawModel } = opts;
const provider = target.provider;
if (
!provider ||
Expand All @@ -130,7 +133,13 @@ function markConnectionLevelExhaustion(
// #5085: empty-content 502 is a healthy connection returning no body — model-level, not
// connection-level. Don't exhaust the provider; let the remaining legs (incl. same-provider)
// be tried in-request.
isEmptyContentFailure(result.status, errorText)
isEmptyContentFailure(result.status, errorText) ||
// Per-model-quota providers (gemini, github, passthrough, compatible) multiplex models
// behind one connection. A model-level 500 (e.g. Gemini "Internal error encountered")
// must NOT exhaust the connection — other models on the same connection may still succeed.
// Other connection-level statuses (408/502/503/504/524) indicate the connection itself is
// bad, so they correctly exhaust even for per-model-quota providers.
(result.status === 500 && hasPerModelQuota(provider, rawModel))
) {
return;
}
Expand Down
Loading
Loading