diff --git a/CHANGELOG.md b/CHANGELOG.md index 7848f169f4..a08b4493dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### 🐛 Bug Fixes +- **fix(executors):** recover from a strict gateway's `context_management: Extra inputs are not permitted` 400. **Claude Code** always sends a top-level `context_management` field; strict anthropic-compatible gateways reject it. The dedicated context-editing 400-fallback only fired when OmniRoute's own `contextEditing` feature was enabled (default off), so a client-sent field passed through untouched and 400'd. `context_management` is now in the generic reactive field-strip list, so it's stripped-and-retried once regardless of the feature flag (with correct request re-signing for claude-compatible relays). Regression guard: `tests/unit/provider-field-strips.test.ts`. (thanks @ohahe52-dot) - **fix(network):** enable **RFC 8305 Happy Eyeballs** (`autoSelectFamily`) on the direct-egress undici dispatcher. When DNS returns both IPv6 (AAAA) and IPv4 (A) and the IPv6 route is broken (e.g. a NAT64 `64:ff9b::` prefix without routing), undici tried IPv6 first and hung until `ETIMEDOUT` (then a 502 + account lockout), even though `curl` reached the same host. The direct dispatcher now races both families and uses whichever connects first. Proxy paths pin family via `proxyTls` and are unaffected. Regression guard: `tests/unit/direct-dispatcher-pipelining-4580.test.ts`. (thanks @adentdk) - **fix(combo):** round-robin now advances the rotation pointer past the model that **actually served**, not the eagerly-scheduled one. With `stickyLimit: 1` (true round-robin), when the scheduled model failed and a *different* model served via fallback, the counter had already advanced +1 from the scheduled index — so the next request reused the fallback-served model, degrading round-robin into hot-spotting on whichever model was healthy. The pointer now advances to the served index + 1 (mirroring the sticky-limit>1 path). Session-stickiness (#3825) and distribution are preserved. Regression guard: `tests/unit/combo-rr-fallback-advance-948.test.ts`. (thanks @binsarjr) - **fix(sse):** a non-string `model` field is now rejected with a `400` before the resolver, instead of crashing downstream `.toLowerCase()`/`.split()` calls into an empty-body `500` that escapes the error sanitizer ([#6407](https://github.com/diegosouzapw/OmniRoute/issues/6407)). Regression guard: `tests/unit/chat-non-string-model-6407.test.ts`. (thanks @chirag127) diff --git a/open-sse/config/providerFieldStrips.ts b/open-sse/config/providerFieldStrips.ts index 0cb5f3f8f5..5a342cc9c1 100644 --- a/open-sse/config/providerFieldStrips.ts +++ b/open-sse/config/providerFieldStrips.ts @@ -1,10 +1,16 @@ // Fields that, when literally named in an upstream 400 body, are safe to strip and // retry once (FCC NIM-style recovery). Mirrors the existing context_management 400 // fallback in base.ts, generalized to these OpenAI-compat / NIM reasoning fields. +// `context_management` (9router#1468): Claude Code sends it top-level; strict +// anthropic-compatible gateways 400 with "context_management: Extra inputs are not +// permitted". The dedicated base.ts fallback only fires when OmniRoute's own +// contextEditing feature is enabled, so a client-sent field passed through +// untouched when the feature is off — this generic strip covers that case. export const KNOWN_OFFENDING_FIELDS: readonly string[] = [ "reasoning_budget", "chat_template", "reasoning_content", + "context_management", ]; /** Return the first known-offending field literally named in a 400 body, or null. */ diff --git a/tests/unit/provider-field-strips.test.ts b/tests/unit/provider-field-strips.test.ts index 7d4b6aa3d5..9e57ca60af 100644 --- a/tests/unit/provider-field-strips.test.ts +++ b/tests/unit/provider-field-strips.test.ts @@ -9,6 +9,12 @@ test("findOffendingField matches known field names in a 400 body", () => { assert.equal(findOffendingField("Invalid argument: reasoning_budget not supported"), "reasoning_budget"); assert.equal(findOffendingField("unexpected field chat_template"), "chat_template"); assert.equal(findOffendingField("reasoning_content is not allowed"), "reasoning_content"); + // #1468: Claude Code's top-level context_management field rejected by strict + // anthropic-compatible gateways → strip + retry regardless of the contextEditing flag. + assert.equal( + findOffendingField("context_management: Extra inputs are not permitted"), + "context_management" + ); assert.equal(findOffendingField("all good"), null); assert.equal(findOffendingField(""), null); });