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 @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -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).",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
}
84 changes: 84 additions & 0 deletions tests/unit/ui/combo-defaults-fusion-5598.test.tsx
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading