diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e7e489afa..a22a7f2564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral - **fix(providers):** `fusion` combo strategy silently returned a panel member's raw answer instead of the configured `config.judgeModel` synthesis ([#6455](https://github.com/diegosouzapw/OmniRoute/issues/6455)) — `handleFusionChat()`'s single-survivor "degrade gracefully" path (added for #6454) returned the lone panel answer directly whenever only one panelist succeeded, regardless of whether an explicit `judgeModel` was configured; with the default `minPanel: 2` and a 2-model panel, any single flaky/rate-limited panelist forced this path on every request, so the configured judge (e.g. `auto/claude-opus`) was never invoked and the client-visible `.model` reflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer whenever `judgeModel` is explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (no `judgeModel` set, where the "judge" is just `panel[0]`). Regression guard: `tests/unit/fusion-judge-model-6455.test.ts` + updated `tests/unit/combo-fusion-strategy.test.ts`. (thanks @chirag127) - **feat(combo):** sanitized diagnostic trace on an auto-combo terminal failure — instead of an opaque 503, a terminal combo failure now returns a whitelist-projected trace (candidate pool size, attempted count, excluded provider/reason codes, attempt order, and a terminal-reason code) via the new `errorResponseWithComboDiagnostics()`/`sanitizeComboDiagnostics()` in `open-sse/utils/error.ts` — provider/model ids and enumerated reason codes only, never keys/tokens/bodies, length- and count-capped. A reasoning-budget-exhausted panel now returns an actionable "increase max_tokens" message rather than a blind retry-limit 503. Regression guard: `tests/unit/combo-diagnostics-trace.test.ts`. ([#6545](https://github.com/diegosouzapw/OmniRoute/pull/6545)) — see PR. (thanks @developerjillur) - **fix(providers):** image/diffusion models discovered from an upstream catalog (e.g. HuggingFace's live `/v1/models`) are no longer advertised as chat models ([#6457](https://github.com/diegosouzapw/OmniRoute/issues/6457)) — the chat catalog builder defaulted synced models with no modality info to `endpoints: ["chat"]`, so `huggingface/stabilityai/stable-diffusion-xl-base-1.0` showed up in the chat `/v1/models` listing and returned `400 "not a chat model"` when called. `catalog.ts` now skips any synced model already registered as an image model for that provider (via the new `isRegisteredImageModel()`), leaving `getAllImageModels()` to list it with the correct `type: "image"`. Regression guard: `tests/unit/image-model-not-in-chat-catalog-6457.test.ts`. +- **fix(resilience):** combo session stickiness never released a pin on a credits-exhausted/banned/expired account, permanently defeating failover for that conversation ([#6692](https://github.com/diegosouzapw/OmniRoute/issues/6692)) — `applySessionStickiness()` (`open-sse/services/combo/sessionStickiness.ts`) gated the sticky pin only on 5h/weekly usage-percentage headroom, which is orthogonal to account availability: a `credits_exhausted`/`banned`/`expired` connection, or one still inside its `rateLimitedUntil` cooldown, reports perfectly healthy headroom, so the pin was force-promoted back to the front of the target list on every subsequent turn. `clearStickyBinding()` also had zero call sites in `combo.ts`'s failure paths, so a quality-validation-rejected 200 (a masked daily-cap refusal) never released the pin either. The gate now also resolves the bound connection's terminal status/cooldown via a new injectable fetcher seam (fail-open on lookup errors, mirroring the existing saturation fetcher), and `combo.ts`'s two dispatchers (`handleComboChat`/`handleRoundRobinCombo`) release the pin immediately at both their connection-exhaustion classification point and their quality-validation-failure branch via the new `releaseStickyPinOnFailure()`. Regression guard: `tests/unit/repro-6692-sticky-terminal.test.ts` + extended `tests/unit/combo-session-stickiness.test.ts`. - **fix(test):** replace the bare `expect(true).toBe(true)` tautology in `playground-api-tab.test.tsx`'s SSE test and close the `check:test-masking` gap that let it slip through for a full cycle ([#6404](https://github.com/diegosouzapw/OmniRoute/issues/6404)) — a prior pass (#6548) had already swapped the literal to `expect(sendBtn).toBeDefined()`, but that stayed just as vacuous: the test's fetch mock returned an empty `/v1/models` list, so `ApiTab`'s Send button is always `disabled` (`!selectedModel`) and the SSE branch never runs — the "SSE infra is verified" comment was never true. The test now mocks a real model, drives the model `