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 @@ -34,6 +34,8 @@

### 🔧 Bug Fixes

- **MITM/cert**: remove the duplicated "Command failed:" prefix in system-command error messages ([#3641](https://github.com/diegosouzapw/OmniRoute/issues/3641)): `execFileText` was prepending its own `"Command failed: "` prefix on top of Node's `execFile` error message, which already begins with `"Command failed: <cmd>"` for non-zero exits. The error message now surfaces Node's message directly (no double prefix), with stderr appended only when non-empty.

- **fix(reasoning): replay `reasoning_content` on plain DeepSeek turns** ([#3632] — thanks @adivekar-utexas): the reasoning-replay gate previously only fired when an assistant message already carried `reasoning_content`. Plain (non-tool-call) turns whose `reasoning_content` was stripped by the client (e.g. Cursor) were forwarded without it, so DeepSeek V4+ rejected the request with 400 "the reasoning_content in the thinking mode must be passed back". The gate now also covers missing/empty `reasoning_content` on DeepSeek replay targets, injecting the cached reasoning (or the non-Anthropic placeholder) so multi-turn text conversations no longer 400. Fixes #1682. 2 regression tests.
- **fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint** ([#3631] — thanks @artickc): Kiro/CodeWhisperer access tokens and Q Developer profile ARNs are region-bound, so enterprise IAM Identity Center accounts outside `us-east-1` (e.g. `eu-central-1`) were rejected by the default host. Adds `resolveKiroRegion` (stored region → profileArn region → `us-east-1`) and `kiroRuntimeHost` (regional `q.{region}.amazonaws.com`, legacy `codewhisperer.us-east-1` for the default), routes chat + usage to the regional endpoint, and discovers the region-matched `profileArn` via `ListAvailableProfiles` in a best-effort `postExchange` hook. 9 tests.
- **fix(combo): skip same-provider/connection targets on connection-level errors** ([#3637] — thanks @herjarsa): on connection-level upstream errors (408/500/502/503/504/524), remaining same-`provider:connection` targets in a combo request are now skipped to avoid hammering a known-bad connection, in both the priority and round-robin paths. Adjusted in review to **exclude OmniRoute circuit-breaker-open responses** (503 + `X-OmniRoute-Provider-Breaker` / `provider_circuit_open`) from this skip, preserving the invariant that a breaker-open is an ordinary target failure (the next same-provider target is still tried). Co-authored with @herjarsa.
Expand Down
7 changes: 6 additions & 1 deletion src/mitm/systemCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export function execFileText(command: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
execFile(command, args, { encoding: "utf8" }, (error, stdout, stderr) => {
if (error) {
reject(new Error(`Command failed: ${getErrorMessage(error)}\n${stderr}`));
// Node's execFile already sets error.message to "Command failed: <cmd>"
// (for non-zero exit) or "spawn <cmd> ENOENT" (for missing binary).
// Re-prefixing with "Command failed: " would double the prefix for the
// non-zero exit case. Surface Node's message directly and only append
// stderr when it contains additional context. (#3641)
reject(new Error(getErrorMessage(error) + (stderr ? `\n${stderr}` : "")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Removing the prefix changes ENOENT error formatting

execFileText now rejects with Node's raw ENOENT message (spawn <cmd> ENOENT) instead of the previous Command failed: spawn <cmd> ENOENT. If callers rely on the prefix for missing binaries, this is an unannounced API-shape change. Consider normalizing only the non-zero-exit path or adding spawn-error coverage if the new shape is intentional.

return;
}
resolve(stdout);
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/mitm-system-commands-prefix-3641.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Regression test for #3641 — `execFileText` produces a doubled
* "Command failed: Command failed: ..." prefix in error messages.
*
* Node's `execFile` already sets `error.message` to
* "Command failed: <command>" when the child process exits non-zero.
* `execFileText` was prepending its own "Command failed: " prefix on top of
* that, producing the doubled string.
*
* The fix: surface `getErrorMessage(error)` directly (Node's message already
* contains the command), only appending stderr when it is non-empty.
*
* Uses `/bin/false` (always exits 1) as the deterministic failing command on
* Linux/macOS. This is the only case that triggers "Command failed: ..." in
* Node's error.message (ENOENT from a missing binary uses "spawn ... ENOENT"
* instead, so it never doubles).
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { execFileText } from "../../src/mitm/systemCommands.ts";

// `/bin/false` exits with code 1 — guaranteed to produce a "Command failed:"
// error.message from Node's execFile, which is exactly the case that was
// being doubled by the bug.
const FALSE_CMD = "/bin/false";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Regression test is not cross-platform

The test hardcodes /bin/false and documents Linux/macOS support only, while OmniRoute has Windows support. This can fail for local Windows runs even though the implementation change is platform-independent. Use a Node-based failing child process such as process.execPath with process.exit(1) args, or explicitly skip the test on Windows.


test("execFileText: error message does NOT contain a doubled 'Command failed:' prefix", async () => {
await assert.rejects(
() => execFileText(FALSE_CMD, []),
(err: unknown) => {
assert.ok(err instanceof Error, "expected an Error");
const msg = err.message;
assert.ok(
!msg.includes("Command failed: Command failed:"),
`Error message contains doubled prefix: ${JSON.stringify(msg)}`,
);
return true;
},
);
});

test("execFileText: error message for a non-zero exit still contains 'Command failed:'", async () => {
await assert.rejects(
() => execFileText(FALSE_CMD, []),
(err: unknown) => {
assert.ok(err instanceof Error, "expected an Error");
// The message should still contain the Node-generated prefix once.
assert.ok(
err.message.includes("Command failed:"),
`Error message should still contain "Command failed:": ${JSON.stringify(err.message)}`,
);
return true;
},
);
});