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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(compression):** the compression settings tab saves only the fields you change, so settings changed on another page or tab (such as Auto-Clarity on the caveman page) keep their values, and a failed save rolls the field back ([#14723](https://github.com/diegosouzapw/OmniRoute/pull/14723)) — thanks @woodsonl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { Card, Button } from "@/shared/components";
import { useTranslations } from "next-intl";
import CompressionTokenSaverCard, {
Expand Down Expand Up @@ -97,6 +97,21 @@ interface RuleMetadata {
description: string;
}

// A save names only the fields it changes, including fields inside nested objects.
type SettingsPatch<T> = {
[K in keyof T]?: T[K] extends unknown[] ? T[K] : T[K] extends object ? SettingsPatch<T[K]> : T[K];
};

const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);

function withPatch<T>(base: T, patch: unknown): T {
if (!isRecord(base) || !isRecord(patch)) return patch as T;
const merged: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(patch)) merged[key] = withPatch(base[key], value);
return merged as T;
}

const MODES: { value: CompressionMode; labelKey: string; descKey: string; icon: string }[] = [
{
value: "off",
Expand Down Expand Up @@ -215,12 +230,22 @@ export default function CompressionSettingsTab() {
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState<"" | "saved" | "error">("");
const [ruleMetadata, setRuleMetadata] = useState<RuleMetadata[]>([]);
// A save sends only the fields it changes, so it never writes back a stale copy of settings
// another page or tab changed after this one loaded. Saves go out one at a time and the form
// shows the last saved config plus the saves still queued, so a failed save rolls back only
// its own fields. A PUT that never settles holds up the saves queued behind it.
const savedRef = useRef(config);
const queuedRef = useRef<SettingsPatch<CompressionConfig>[]>([]);
const saveQueueRef = useRef(Promise.resolve());

useEffect(() => {
fetch("/api/settings/compression")
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
if (data) setConfig(data);
if (data) {
savedRef.current = data;
setConfig(data);
}
})
.catch(() => {})
.finally(() => setLoading(false));
Expand All @@ -232,28 +257,52 @@ export default function CompressionSettingsTab() {
.catch(() => {});
}, []);

const save = async (updates: Partial<CompressionConfig>) => {
const newConfig = { ...config, ...updates };
setConfig(newConfig);
const save = (updates: SettingsPatch<CompressionConfig>) => {
const showQueued = () =>
setConfig(
queuedRef.current.reduce<CompressionConfig>(
(shown, queued) => withPatch(shown, queued),
savedRef.current
)
);
queuedRef.current.push(updates);
showQueued();
setSaving(true);
setStatus("");
try {
const res = await fetch("/api/settings/compression", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newConfig),
});
if (res.ok) {
setStatus("saved");
setTimeout(() => setStatus(""), 2000);
} else {
setStatus("error");
}
} catch {
setStatus("error");
} finally {
setSaving(false);
}
saveQueueRef.current = saveQueueRef.current.then(async () => {
// The server stores each nested object as one row, so a PUT carries the whole object.
// Its other fields come from the server's current row, not this tab's copy, so a sibling
// field saved elsewhere since the tab loaded is not written back.
const current: CompressionConfig | null = Object.values(updates).some(isRecord)
? await fetch("/api/settings/compression")
.then((res) => (res.ok ? res.json() : null))
.catch(() => null)
: savedRef.current;
const merged = current && withPatch(current, updates);
const body =
merged &&
Object.fromEntries(
Object.keys(updates).map((key) => [key, merged[key as keyof CompressionConfig]])
);
const ok =
!!body &&
(await fetch("/api/settings/compression", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}).then(
(res) => res.ok,
() => false
));
queuedRef.current.shift();
if (ok) savedRef.current = { ...savedRef.current, ...body };
showQueued();
setSaving(queuedRef.current.length > 0);
// A failure stays on screen until the next edit, so a queued success or an earlier
// save's timeout cannot hide a field that just rolled back.
setStatus((shown) => (ok ? (shown === "error" ? shown : "saved") : "error"));
if (ok) setTimeout(() => setStatus((shown) => (shown === "saved" ? "" : shown)), 2000);
});
};

const toggleCavemanRole = (role: "user" | "assistant" | "system") => {
Expand All @@ -262,7 +311,7 @@ export default function CompressionSettingsTab() {
? currentRoles.filter((r) => r !== role)
: [...currentRoles, role];
save({
cavemanConfig: { ...config.cavemanConfig!, compressRoles: newRoles },
cavemanConfig: { compressRoles: newRoles },
});
};

Expand All @@ -272,7 +321,7 @@ export default function CompressionSettingsTab() {
? currentSkip.filter((r) => r !== rule)
: [...currentSkip, rule];
save({
cavemanConfig: { ...config.cavemanConfig!, skipRules: newSkip },
cavemanConfig: { skipRules: newSkip },
});
};

Expand Down Expand Up @@ -494,7 +543,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
cavemanConfig: {
...config.cavemanConfig!,
minMessageLength: parseInt(e.target.value) || 50,
},
})
Expand Down Expand Up @@ -540,7 +588,6 @@ export default function CompressionSettingsTab() {
.filter(Boolean);
save({
cavemanConfig: {
...config.cavemanConfig!,
preservePatterns: patterns,
},
});
Expand Down Expand Up @@ -573,7 +620,6 @@ export default function CompressionSettingsTab() {
onClick={() =>
save({
cavemanOutputMode: {
...config.cavemanOutputMode!,
autoClarity: !config.cavemanOutputMode!.autoClarity,
},
})
Expand Down Expand Up @@ -611,7 +657,6 @@ export default function CompressionSettingsTab() {
onClick={() =>
save({
aggressive: {
...config.aggressive!,
summarizerEnabled: !config.aggressive!.summarizerEnabled,
},
})
Expand Down Expand Up @@ -639,7 +684,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
aggressive: {
...config.aggressive!,
maxTokensPerMessage: parseInt(e.target.value) || 2048,
},
})
Expand All @@ -662,7 +706,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
aggressive: {
...config.aggressive!,
minSavingsThreshold: parseFloat(e.target.value) || 0.05,
},
})
Expand Down Expand Up @@ -695,9 +738,7 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
aggressive: {
...config.aggressive!,
thresholds: {
...config.aggressive!.thresholds,
[tier]: parseInt(e.target.value) || 2,
},
},
Expand All @@ -722,9 +763,7 @@ export default function CompressionSettingsTab() {
onClick={() =>
save({
aggressive: {
...config.aggressive!,
toolStrategies: {
...config.aggressive!.toolStrategies,
[strategy]: !config.aggressive!.toolStrategies[strategy],
},
},
Expand Down Expand Up @@ -757,7 +796,6 @@ export default function CompressionSettingsTab() {
onClick={() =>
save({
ultra: {
...config.ultra!,
enabled: !config.ultra!.enabled,
},
})
Expand Down Expand Up @@ -785,7 +823,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
ultra: {
...config.ultra!,
compressionRate: parseFloat(e.target.value) || 0,
},
})
Expand All @@ -805,7 +842,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
ultra: {
...config.ultra!,
minScoreThreshold: parseFloat(e.target.value) || 0,
},
})
Expand All @@ -825,7 +861,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
ultra: {
...config.ultra!,
maxTokensPerMessage: parseInt(e.target.value) || 0,
},
})
Expand All @@ -842,7 +877,6 @@ export default function CompressionSettingsTab() {
onClick={() =>
save({
ultra: {
...config.ultra!,
slmFallbackToAggressive: !config.ultra!.slmFallbackToAggressive,
},
})
Expand All @@ -867,7 +901,6 @@ export default function CompressionSettingsTab() {
onChange={(e) =>
save({
ultra: {
...config.ultra!,
modelPath: e.target.value.trim() || undefined,
},
})
Expand Down
Loading
Loading