mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-14 19:22:32 +03:00
fix(executors): strip client_metadata on the OpenCode path (port from 9router#1442)
OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) return 400 "Extra inputs are not permitted, field: 'client_metadata'" — an OpenAI-Codex/Claude-CLI passthrough field with no equivalent upstream. The DefaultExecutor strip only covers cerebras/mistral, and OpencodeExecutor extends BaseExecutor directly, so nothing removed it on this path. Strip it in OpencodeExecutor.transformRequest. Regression guard: tests/unit/opencode-strip-client-metadata-1442.test.ts. Reported-by: yanpaing007 (https://github.com/decolua/9router/issues/1442)
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(executors):** strip the `client_metadata` passthrough field on the **OpenCode** path. OpenCode upstreams (e.g. `kimi-k2.6` via opencode-go) reject it with `400 "Extra inputs are not permitted, field: 'client_metadata'"`; the DefaultExecutor strip only covered cerebras/mistral and `OpencodeExecutor` extends `BaseExecutor` directly, so nothing removed it there. Regression guard: `tests/unit/opencode-strip-client-metadata-1442.test.ts`. (thanks @yanpaing007)
|
||||
|
||||
- **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).
|
||||
|
||||
@@ -292,6 +292,19 @@ export class OpencodeExecutor extends BaseExecutor {
|
||||
credentials: ProviderCredentials
|
||||
): any {
|
||||
let modifiedBody = super.transformRequest(model, body, stream, credentials);
|
||||
// 9router#1442: OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) return
|
||||
// 400 "Extra inputs are not permitted, field: 'client_metadata'" — an
|
||||
// OpenAI-Codex/Claude-CLI passthrough field with no equivalent here. The
|
||||
// DefaultExecutor strip only covers cerebras/mistral, and OpencodeExecutor
|
||||
// extends BaseExecutor directly, so nothing removed it on this path.
|
||||
if (
|
||||
modifiedBody &&
|
||||
typeof modifiedBody === "object" &&
|
||||
!Array.isArray(modifiedBody) &&
|
||||
Object.prototype.hasOwnProperty.call(modifiedBody, "client_metadata")
|
||||
) {
|
||||
delete (modifiedBody as Record<string, unknown>).client_metadata;
|
||||
}
|
||||
if (
|
||||
modifiedBody &&
|
||||
typeof modifiedBody === "object" &&
|
||||
|
||||
52
tests/unit/opencode-strip-client-metadata-1442.test.ts
Normal file
52
tests/unit/opencode-strip-client-metadata-1442.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts");
|
||||
|
||||
/**
|
||||
* Regression test for upstream decolua/9router#1442:
|
||||
*
|
||||
* OpenCode upstreams (e.g. kimi-k2.6 via opencode-go) reject the
|
||||
* `client_metadata` passthrough field (an OpenAI-Codex/Claude-CLI artifact)
|
||||
* with 400 "Extra inputs are not permitted, field: 'client_metadata'".
|
||||
* DefaultExecutor strips it only for cerebras/mistral, and OpencodeExecutor
|
||||
* extends BaseExecutor directly, so nothing removed it on the opencode path.
|
||||
* OpencodeExecutor.transformRequest must strip it.
|
||||
*/
|
||||
describe("OpencodeExecutor — strips client_metadata (#1442)", () => {
|
||||
const executor = new OpencodeExecutor("opencode-go");
|
||||
const CREDENTIALS = { apiKey: "k" } as Record<string, unknown>;
|
||||
|
||||
function body() {
|
||||
return {
|
||||
model: "oc/kimi-k2.6",
|
||||
stream: true,
|
||||
client_metadata: { user_id: "abc" },
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
}
|
||||
|
||||
it("removes client_metadata from the forwarded body", () => {
|
||||
const out = executor.transformRequest("oc/kimi-k2.6", body(), true, CREDENTIALS) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(
|
||||
Object.prototype.hasOwnProperty.call(out, "client_metadata"),
|
||||
false,
|
||||
"opencode forward body must not carry client_metadata"
|
||||
);
|
||||
assert.ok(Array.isArray(out.messages), "messages preserved");
|
||||
});
|
||||
|
||||
it("is a no-op when client_metadata is absent", () => {
|
||||
const b = body();
|
||||
delete (b as Record<string, unknown>).client_metadata;
|
||||
const out = executor.transformRequest("oc/kimi-k2.6", b, true, CREDENTIALS) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal("client_metadata" in out, false);
|
||||
assert.ok(Array.isArray(out.messages));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user