fix: surface fusion-specific config on the Global Routing tab (#5598) (#5688)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-30 13:54:58 -03:00
committed by GitHub
parent bfcfc9bf3f
commit 24ae0b32f8
5 changed files with 185 additions and 1 deletions

View File

@@ -10,6 +10,8 @@
### 🔧 Bug Fixes
- **dashboard (routing):** selecting the **fusion** strategy on the Global Routing defaults tab now reveals fusion-specific config instead of only the generic resilience fields. Fusion's engine knobs — `judgeModel` (the model that synthesizes the panel answers) and `fusionTuning` (`minPanel` / `stragglerGraceMs` / `panelHardTimeoutMs`) — already existed in the schema and the per-combo editor, but the Global Routing tab never surfaced them, so picking "fusion" there was effectively a no-op. The fields are now shown (extracted into a new `FusionDefaultsFields` component). Voting / aggregation-mode / per-provider-weight are intentionally not shown — those don't exist in the fusion engine. Regression guard: `tests/unit/ui/combo-defaults-fusion-5598.test.tsx`. ([#5598](https://github.com/diegosouzapw/OmniRoute/issues/5598))
- **dashboard (free proxy pool):** the free proxy pool "Sync All" no longer fails silently with `Total: 0`. Three fixes: (1) the **IPLocate** source fetched `…/protocols/<proto>.json` and parsed it as JSON, but the upstream list is plain text (`<proto>.txt`, one `ip:port` per line) — every protocol 404'd / failed to parse; it now fetches `.txt` and parses the line list. (2) The sync route **isolates each source** in its own try/catch, so one provider throwing (e.g. a TLS handshake failure) no longer aborts the whole sync — the working sources still populate the pool. (3) The UI now **surfaces the per-source errors** the route already returns, instead of discarding the response, so a partial/empty sync explains itself. Regression guards: `tests/unit/free-proxy-providers.test.ts`, `tests/unit/proxy-pool-sync-4878.test.ts`, `tests/unit/free-pool-tab.test.tsx`. ([#5595](https://github.com/diegosouzapw/OmniRoute/issues/5595))
- **dashboard (memory engine):** the memory engine status page no longer mixes English and Portuguese. The embedding / vector-store / rerank **status detail strings** were hardcoded in Portuguese in the backend (`resolveEmbeddingSource`, `engineStatus`), e.g. `auto: nenhuma fonte de embedding disponível` and `sqlite-vec ativo, dim=…`, while the surrounding UI labels render from the English i18n bundle — so an English user saw a half-translated page. The backend detail strings are now English (`auto: no embedding source available`, `sqlite-vec active, dim=…`, etc.), matching the rest of the page. Regression guard: `tests/unit/memory-engine-status.test.ts`. ([#5596](https://github.com/diegosouzapw/OmniRoute/issues/5596))

View File

@@ -1,5 +1,6 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_30_5598_fusion_defaults": "PR #5598 own growth: ComboDefaultsTab.tsx 846->851 (+5 = import + the `strategy === \"fusion\"` conditional that mounts the new FusionDefaultsFields component). The bulk of the fusion-defaults UI (judgeModel + fusionTuning inputs) was EXTRACTED to a new src/app/(dashboard)/dashboard/settings/components/FusionDefaultsFields.tsx (92 LOC, <cap) to keep the frozen god-file's growth to the minimal wiring. Mirrors the per-combo Fusion editor already rebaselined under #5074 (combos/page.tsx +109). Covered by tests/unit/ui/combo-defaults-fusion-5598.test.tsx.",
"_rebaseline_2026_06_29_v3841_release": "callLogs 985->997 + chat 1632->1635 + chatHelpers 811->842 (#5351 opencode visible-rotation-logs/ProxyEgress/SQL-vars), base.ts 1475->1497 + openai-to-claude 805->823 (#5352 thinking hydrate/redacted-replay + #5342 empty-messages guard), accountFallback 1777->1783 (#5346 402 auto-disable depleted key) + account-fallback-service.test 1569->1572 (#5346 test), provider-validation-specialty.test 2801->2843 (#5358 grok cf_clearance + #5337 gemini catalog). Cycle-close drift from merged campaign PRs absorbed at release close per generate-release Phase 0 (the PR->release fast-gates do NOT run check:file-size); all irreducible chokepoint/feature growth next to existing branches, covered by per-PR tests. Structural shrink tracked under decomposition roadmap #3501.",
"_rebaseline_2026_06_26_v3838_ownerprs_batch": "Lote /review-prs v3.8.38 (PRs do dono + contribuidores): src/lib/db/providers.ts 1093->1107 (+14 = #5121 cookie-dedup branch extraido para o helper findExistingCookieConnection — a extracao reduz a complexidade ciclomatica de createProviderConnection mas adiciona ~14 linhas ao arquivo; trade-off consciente complexity-vs-file-size) e src/lib/usage/usageHistory.ts 934->983 (+49 = #4940 dedup guard SELECT-before-INSERT + endpoint backfill + scheduleStatsEvent debounce). Crescimento de feature/fix legitimo recem-TDD'd; o fast-path PR->release nao roda check:file-size, entao o ramo acumula sem rebaselinar. Reducao estrutural fica como debt (#3501).",
"_rebaseline_2026_06_26_relgreen_db_test": "Release-green follow-up: tests/unit/db-core-init.test.ts 867->877 (+10 = the invalid-DATA_DIR test now captures the rejection and asserts both Error type AND message — restores net-neutral assert count after #5117's consolidation, satisfying check:test-masking — instead of a single assert.rejects).",
@@ -201,7 +202,7 @@
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819,
"src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 846,
"src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 851,
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974,
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898,
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,

View File

@@ -3,6 +3,7 @@
import { useState, useEffect, useRef } from "react";
import { Card, Button, Input, Toggle } from "@/shared/components";
import { cn } from "@/shared/utils/cn";
import FusionDefaultsFields from "./FusionDefaultsFields";
import {
ROUTING_STRATEGIES,
SETTINGS_FALLBACK_STRATEGY_VALUES,
@@ -647,6 +648,10 @@ export default function ComboDefaultsTab() {
</div>
)}
{comboDefaults.strategy === "fusion" && (
<FusionDefaultsFields comboDefaults={comboDefaults} setComboDefaults={setComboDefaults} />
)}
{/* Toggles */}
<div className="flex flex-col gap-3 pt-3 border-t border-border/50">
<div className="flex items-center justify-between">

View File

@@ -0,0 +1,92 @@
import { Input } from "@/shared/components";
import { useTranslations } from "next-intl";
function translateOrFallback(
t: ReturnType<typeof useTranslations>,
key: string,
fallback: string
): string {
return typeof t.has === "function" && t.has(key) ? t(key) : fallback;
}
interface FusionDefaultsFieldsProps {
comboDefaults: any;
setComboDefaults: (updater: (prev: any) => any) => void;
}
/**
* #5598 — Fusion-specific defaults for the Global Routing tab. Selecting the
* "fusion" strategy previously showed only the generic resilience fields even
* though fusion has real engine knobs (`open-sse/services/fusion.ts`, schema in
* `src/shared/validation/schemas/combo.ts`): `judgeModel` synthesizes the final
* answer and `fusionTuning` controls the quorum-grace panel collection. The
* per-combo editor already exposes these; this surfaces the same knobs as global
* defaults. (Voting / aggregation-mode / per-provider-weight do not exist in the
* engine, so they are intentionally not shown.)
*/
export default function FusionDefaultsFields({
comboDefaults,
setComboDefaults,
}: FusionDefaultsFieldsProps) {
const t = useTranslations("settings");
const setTuning = (patch: Record<string, number | undefined>) =>
setComboDefaults((prev: any) => ({
...prev,
fusionTuning: { ...prev.fusionTuning, ...patch },
}));
const num = (value: string) => (value ? Number(value) : undefined);
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pt-3 border-t border-border/50">
<Input
label={translateOrFallback(t, "fusionJudgeModel", "Judge Model")}
type="text"
value={comboDefaults.judgeModel ?? ""}
placeholder="openai/gpt-5.5"
onChange={(e) =>
setComboDefaults((prev: any) => ({ ...prev, judgeModel: e.target.value || undefined }))
}
className="text-sm md:col-span-2"
/>
<Input
label={translateOrFallback(t, "fusionMinPanel", "Min Panel")}
type="number"
min={1}
max={50}
value={comboDefaults.fusionTuning?.minPanel ?? ""}
placeholder="2"
onChange={(e) => setTuning({ minPanel: num(e.target.value) })}
className="text-sm"
/>
<Input
label={translateOrFallback(t, "fusionStragglerGraceMs", "Straggler Grace (ms)")}
type="number"
min={0}
max={120000}
value={comboDefaults.fusionTuning?.stragglerGraceMs ?? ""}
placeholder="8000"
onChange={(e) => setTuning({ stragglerGraceMs: num(e.target.value) })}
className="text-sm"
/>
<Input
label={translateOrFallback(t, "fusionPanelHardTimeoutMs", "Panel Hard Timeout (ms)")}
type="number"
min={1000}
max={600000}
value={comboDefaults.fusionTuning?.panelHardTimeoutMs ?? ""}
placeholder="90000"
onChange={(e) => setTuning({ panelHardTimeoutMs: num(e.target.value) })}
className="text-sm md:col-span-2"
/>
<div className="md:col-span-2 rounded-lg border border-blue-500/20 bg-blue-500/5 p-3">
<p className="text-xs text-blue-700 dark:text-blue-300">
{translateOrFallback(
t,
"fusionDefaultsNote",
"Fusion fans out to all of a combo's models and a judge model synthesizes the final answer (defaults to the first panel model when unset). These are global defaults for new or unconfigured combos."
)}
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,84 @@
// @vitest-environment jsdom
//
// #5598 — Selecting the "fusion" routing strategy on the Global Routing defaults
// tab previously revealed no fusion-specific config (only the generic resilience
// fields). Fusion's engine knobs (judgeModel + fusionTuning) exist in the schema
// and the per-combo editor, but were never surfaced as global defaults. This
// asserts they now appear when fusion is the selected strategy.
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
// identity translator with no `has`, so translateOrFallback uses the English fallbacks
useTranslations: () => (key: string) => key,
}));
const { default: ComboDefaultsTab } = await import(
"../../../src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab"
);
function okJson(data: unknown) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(data) } as Response);
}
function setupFetch(strategy: string) {
vi.stubGlobal(
"fetch",
vi.fn((url: string) => {
if (String(url).includes("/combo-defaults")) return okJson({ comboDefaults: { strategy } });
if (String(url).includes("/api/providers")) return okJson({ connections: [] });
return okJson({}); // /api/settings
})
);
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderTab() {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<ComboDefaultsTab />);
});
containers.push({ root, el });
return el;
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("ComboDefaultsTab fusion config (#5598)", () => {
it("shows fusion-specific fields when the fusion strategy is selected", async () => {
setupFetch("fusion");
const el = renderTab();
// RED before the fix: fusion had no config block → these labels never render.
await waitFor(() => el.textContent?.includes("Judge Model") === true);
expect(el.textContent).toContain("Judge Model");
expect(el.textContent).toContain("Min Panel");
expect(el.textContent).toContain("Straggler Grace (ms)");
expect(el.textContent).toContain("Panel Hard Timeout (ms)");
});
it("does not show fusion fields for a non-fusion strategy", async () => {
setupFetch("priority");
const el = renderTab();
// Let the component settle (load + render).
await new Promise((r) => setTimeout(r, 200));
expect(el.textContent).not.toContain("Judge Model");
});
});