mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 11:43:10 +03:00
fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584)
This commit is contained in:
@@ -23,6 +23,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584)
|
||||
- **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7)
|
||||
- **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa)
|
||||
- **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer <apiKey>` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy).
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// (fallback to the canonical vocabulary), resolved by `resolveReasoningControls`.
|
||||
|
||||
import type { PlaygroundParams } from "./ParamSliders";
|
||||
import type { ReasoningControlSpec } from "./reasoningControls";
|
||||
import type { ReasoningControlSpec } from "./reasoningControlUtils";
|
||||
|
||||
interface ReasoningControlsProps {
|
||||
spec: ReasoningControlSpec;
|
||||
|
||||
@@ -20,7 +20,7 @@ import ReasoningControls from "./ReasoningControls";
|
||||
import {
|
||||
resolveReasoningControls,
|
||||
type ReasoningControlSpec,
|
||||
} from "./reasoningControls";
|
||||
} from "./reasoningControlUtils";
|
||||
|
||||
export interface ConfigState {
|
||||
endpoint: PlaygroundEndpoint;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useStreamMetrics } from "../../hooks/useStreamMetrics";
|
||||
import { getModelPricing } from "@/lib/playground/types";
|
||||
import type { ConfigState } from "../StudioConfigPane";
|
||||
import type { StreamMetrics } from "@/shared/schemas/playground";
|
||||
import { buildReasoningRequestFields } from "../reasoningControls";
|
||||
import { buildReasoningRequestFields } from "../reasoningControlUtils";
|
||||
|
||||
interface Message {
|
||||
role: "system" | "user" | "assistant";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { compareTr } from "@/shared/utils/turkishText";
|
||||
import type { ModelReasoningCapabilities } from "@/app/(dashboard)/dashboard/playground/components/reasoningControls";
|
||||
import type { ModelReasoningCapabilities } from "@/app/(dashboard)/dashboard/playground/components/reasoningControlUtils";
|
||||
|
||||
/**
|
||||
* Prefix-based format→model matching, used to pick a smart default
|
||||
|
||||
78
tests/unit/case-collision-6584.test.ts
Normal file
78
tests/unit/case-collision-6584.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Regression guard for issue #6584: webpack (OMNIROUTE_USE_TURBOPACK=0) build breaks on
|
||||
// case-insensitive filesystems (macOS APFS default, Windows) because two source
|
||||
// files in the same directory differ only by casing:
|
||||
// src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx
|
||||
// src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts
|
||||
// On a case-sensitive FS (Linux, this sandbox) both files resolve fine, so this
|
||||
// test does not literally reproduce the webpack warning/build error. Instead it
|
||||
// encodes the same-directory case-collision root cause as a filesystem-level
|
||||
// invariant check that fails deterministically on ANY OS: two distinct files
|
||||
// whose relative path is identical after lower-casing (stem, ignoring extension,
|
||||
// since extensionless imports are what collide) must not coexist in the same
|
||||
// directory tree.
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
const SCAN_DIRS = ["src", "open-sse"];
|
||||
const EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
||||
const IGNORE_DIR_NAMES = new Set(["node_modules", ".next", "dist", "build", ".git"]);
|
||||
|
||||
function walk(dir: string, out: string[]): void {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (IGNORE_DIR_NAMES.has(entry.name)) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walk(full, out);
|
||||
} else if (EXTENSIONS.has(path.extname(entry.name))) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("#6584: no two source files in the same directory differ only by casing", () => {
|
||||
const files: string[] = [];
|
||||
for (const d of SCAN_DIRS) {
|
||||
const abs = path.join(ROOT, d);
|
||||
if (fs.existsSync(abs)) walk(abs, files);
|
||||
}
|
||||
|
||||
// Group by (directory, lower-cased basename WITHOUT extension). Extensionless
|
||||
// imports (e.g. `from "./reasoningControls"`) are resolved by webpack/Node's
|
||||
// module resolver trying several extensions; on a case-insensitive filesystem
|
||||
// that resolution collapses "ReasoningControls.tsx" and "reasoningControls.ts"
|
||||
// into the same candidate, so the stem — not the full filename — is what must
|
||||
// stay unique per directory regardless of extension.
|
||||
const byDirLowerStem = new Map<string, string[]>();
|
||||
for (const f of files) {
|
||||
const dir = path.dirname(f);
|
||||
const ext = path.extname(f);
|
||||
const stem = path.basename(f, ext);
|
||||
const lowerStem = stem.toLowerCase();
|
||||
const key = `${dir}::${lowerStem}`;
|
||||
const arr = byDirLowerStem.get(key) ?? [];
|
||||
arr.push(f);
|
||||
byDirLowerStem.set(key, arr);
|
||||
}
|
||||
|
||||
const collisions: string[][] = [];
|
||||
for (const arr of byDirLowerStem.values()) {
|
||||
const distinctStems = new Set(arr.map((f) => path.basename(f, path.extname(f))));
|
||||
if (distinctStems.size > 1) {
|
||||
collisions.push(arr);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
collisions,
|
||||
[],
|
||||
`Found case-only filename collisions (breaks webpack builds on case-insensitive filesystems): ${JSON.stringify(collisions, null, 2)}`
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolveReasoningControls,
|
||||
buildReasoningRequestFields,
|
||||
type ReasoningControlSpec,
|
||||
} from "../../src/app/(dashboard)/dashboard/playground/components/reasoningControls.ts";
|
||||
} from "../../src/app/(dashboard)/dashboard/playground/components/reasoningControlUtils.ts";
|
||||
import { CANONICAL_EFFORT_VALUES } from "../../src/shared/reasoning/effortStandardization.ts";
|
||||
|
||||
// #6241: the Playground effort selector + thinking toggle read a model's `supportsThinking` /
|
||||
|
||||
Reference in New Issue
Block a user