feat(dashboard): Stream Error Patterns editor on provider page

Per-provider textarea (one pattern per line: plain text or /regex/flags)
saved via /api/settings as streamErrorPatterns. Mirrors the existing
providerTimeouts load/save pattern.
This commit is contained in:
2026-08-04 23:35:25 +07:00
parent 34a78f579e
commit 9bd7adc556

View File

@@ -96,6 +96,7 @@ export default function ProviderDetailPage() {
const [providerStickyLimit, setProviderStickyLimit] = useState("");
const [thinkingMode, setThinkingMode] = useState("auto");
const [providerTimeout, setProviderTimeout] = useState("");
const [streamErrorPatternsText, setStreamErrorPatternsText] = useState("");
const [autoPing, setAutoPing] = useState({ enabled: false, connections: {} });
const [suggestedModels, setSuggestedModels] = useState([]);
const [liveModels, setLiveModels] = useState([]);
@@ -444,6 +445,12 @@ export default function ProviderDetailPage() {
setProviderTimeout(
timeoutCfg.timeoutMs != null ? String(timeoutCfg.timeoutMs) : "",
);
// Load per-provider stream error patterns (one per line)
setStreamErrorPatternsText(
((settingsData.streamErrorPatterns || {})[providerId] || []).join(
"\n",
),
);
const autoPingSettingsKey = AUTO_PING_SETTINGS_KEYS[providerId];
const apCfg = autoPingSettingsKey
? settingsData[autoPingSettingsKey] || {}
@@ -595,6 +602,30 @@ export default function ProviderDetailPage() {
}
};
const saveStreamErrorPatterns = async () => {
try {
const settingsRes = await fetch("/api/settings", {
cache: "no-store",
});
const settingsData = settingsRes.ok ? await settingsRes.json() : {};
const current = settingsData.streamErrorPatterns || {};
const updated = { ...current };
const lines = streamErrorPatternsText
.split("\n")
.map((s) => s.trim())
.filter(Boolean);
if (lines.length > 0) updated[providerId] = lines;
else delete updated[providerId];
await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ streamErrorPatterns: updated }),
});
} catch (error) {
console.log("Error saving stream error patterns:", error);
}
};
const handleTimeoutChange = (value) => {
const cleaned = value.replace(/[^0-9]/g, "");
setProviderTimeout(cleaned);
@@ -2459,6 +2490,38 @@ export default function ProviderDetailPage() {
</Card>
)}
{/* Stream Error Patterns */}
<Card>
<div className="mb-3">
<h2 className="text-lg font-semibold">Stream Error Patterns</h2>
<p className="mt-1 text-sm text-text-muted">
Treat HTTP-200 streams whose first bytes match one of these
patterns as failed requests (enables fallback + FAILED logs). One
pattern per line: plain text (case-insensitive substring) or{" "}
<code className="rounded bg-background px-1 py-0.5 font-mono text-xs">
/regex/flags
</code>
. Only the first few KB of the stream are scanned.
</p>
</div>
<textarea
value={streamErrorPatternsText}
onChange={(e) => setStreamErrorPatternsText(e.target.value)}
rows={5}
placeholder={"server_error\n/network connection lost/i"}
className="w-full rounded-md border border-border bg-background p-2 font-mono text-xs focus:border-primary focus:outline-none"
/>
<div className="mt-2">
<Button
size="sm"
onClick={saveStreamErrorPatterns}
className="w-full sm:w-auto"
>
Save Patterns
</Button>
</div>
</Card>
{/* Models */}
<Card>
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">