fix(executors): strip client context_management on 400 (port from 9router#1468)

Claude Code always sends a top-level `context_management` field. Strict
anthropic-compatible gateways reject it with 400 "context_management: Extra
inputs are not permitted". The dedicated context-editing 400-fallback in
base.ts only fires when OmniRoute's own `contextEditing` feature is enabled
(default off), so a client-sent field passed through untouched and 400'd with
no recovery. Add `context_management` to KNOWN_OFFENDING_FIELDS so the generic
reactive 400 field-downgrade strips-and-retries it once, independent of the
feature flag (the generic path already re-signs for claude-compatible relays).

Regression guard: tests/unit/provider-field-strips.test.ts.

Reported-by: ohahe52-dot (https://github.com/decolua/9router/issues/1468)
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 16:53:43 -03:00
parent fb3da7ce98
commit b6f2bd85fe
3 changed files with 14 additions and 0 deletions

View File

@@ -20,6 +20,8 @@
### 🐛 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(live-ws):** the Live Dashboard WebSocket server now **rejects on bind failure** (e.g. `EADDRINUSE` when the API bridge already holds the port) instead of letting the error surface as an unhandled `error` event that crash-loops the process — the `error` listener is attached to `wss` (not `server`) and releases the EventBus subscription on a failed start ([#6324](https://github.com/diegosouzapw/OmniRoute/issues/6324)). Regression guard: `tests/unit/live-ws-eaddrinuse-6324.test.ts`. (thanks @vinayakkulkarni)
- **fix(dashboard):** the Home provider-topology widget now trusts the live provider-metrics snapshot — it uses `topology.errorProvider` and live `activeRequests` directly instead of re-deriving state from a stale `lastErrorAt` or applying a frontend timeout filter, so the topology reflects real-time provider health. Regression guard: `tests/unit/home-provider-topology-live-state.test.ts`. (thanks @xz-dev)
- **fix(sse):** strip zero-width markers from streamed **tool-call arguments** — a follow-up to [#5857](https://github.com/diegosouzapw/OmniRoute/pull/5857). That PR removed injected zero-width joiners (U+200D) from streamed assistant text/reasoning but deliberately left tool-call argument JSON byte-exact. The request-side obfuscation (`open-sse/services/claudeCodeObfuscation.ts`) injects ZWJ into agent words — including the temp path inside the Bash tool description — and Claude models copy that verbatim into generated commands, which are delivered as tool-call arguments rather than assistant text. As a result the ZWJ survived and corrupted code blocks (e.g. a temp path rendered with an invisible joiner). Now `open-sse/handlers/responseSanitizer.ts` strips zero-width code points from tool-call argument strings at every emit site (OpenAI non-stream/stream chat `tool_calls` + legacy `function_call`, native Responses `function_call` items, the OpenAI→Responses conversion, and the native Responses streaming `response.function_call_arguments.delta/.done` events). Only zero-width code points are removed; JSON structure and all other bytes stay identical (no parse/restringify), so normal arguments remain byte-exact. Regression guard: 6 new cases in `tests/unit/response-sanitizer.test.ts` (suite 50/50).

View File

@@ -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. */

View File

@@ -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);
});