feat(open-sse): streamErrorPatterns matching util (text + regex)
This commit is contained in:
47
open-sse/utils/streamErrorPatterns.js
Normal file
47
open-sse/utils/streamErrorPatterns.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Config-driven in-stream error pattern matching.
|
||||
* See docs/2026-08-04-stream-error-patterns-design.md.
|
||||
*
|
||||
* Settings shape: streamErrorPatterns: { [provider]: string[] }
|
||||
* Pattern syntax per entry:
|
||||
* - "plain text" → case-insensitive substring match
|
||||
* - "/regex/flags" → RegExp match (flags after the last "/")
|
||||
*/
|
||||
|
||||
export function parsePatterns(patterns) {
|
||||
if (!Array.isArray(patterns)) return [];
|
||||
const out = [];
|
||||
for (const entry of patterns) {
|
||||
if (typeof entry !== "string" || !entry.trim()) continue;
|
||||
const raw = entry.trim();
|
||||
const m = raw.match(/^\/(.*)\/([a-z]*)$/s);
|
||||
if (m) {
|
||||
try {
|
||||
out.push({ regex: new RegExp(m[1], m[2]), raw });
|
||||
} catch {
|
||||
// invalid regex → skip; config errors must never break requests
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.push({ text: raw.toLowerCase(), raw });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function matchStreamErrorPatterns(patterns, text) {
|
||||
if (!Array.isArray(patterns) || patterns.length === 0) return null;
|
||||
if (typeof text !== "string" || !text) return null;
|
||||
const lower = text.toLowerCase();
|
||||
for (const p of parsePatterns(patterns)) {
|
||||
if (p.text && lower.includes(p.text)) return p.raw;
|
||||
if (p.regex) {
|
||||
p.regex.lastIndex = 0; // stateless even with /g
|
||||
if (p.regex.test(text)) return p.raw;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function streamStatusForContent(patterns, content) {
|
||||
return matchStreamErrorPatterns(patterns, content) ? "error" : "success";
|
||||
}
|
||||
50
tests/unit/streamErrorPatterns.test.js
Normal file
50
tests/unit/streamErrorPatterns.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parsePatterns, matchStreamErrorPatterns, streamStatusForContent } from "../../open-sse/utils/streamErrorPatterns.js";
|
||||
|
||||
describe("streamErrorPatterns util", () => {
|
||||
it("matches plain text as case-insensitive substring", () => {
|
||||
expect(matchStreamErrorPatterns(["Network connection lost"], "network CONNECTION LOST.")).toBe("Network connection lost");
|
||||
expect(matchStreamErrorPatterns(["server_error"], "some normal content")).toBeNull();
|
||||
});
|
||||
|
||||
it("matches /regex/flags", () => {
|
||||
expect(matchStreamErrorPatterns(["/generation failed.*retry/i"], "GENERATION FAILED. please RETRY")).toBe("/generation failed.*retry/i");
|
||||
expect(matchStreamErrorPatterns(["/\\d+ tokens/"], "used 123 tokens")).toBe("/\\d+ tokens/");
|
||||
});
|
||||
|
||||
it("skips invalid regex and empty entries", () => {
|
||||
expect(matchStreamErrorPatterns(["/[unclosed/", "", " "], "anything")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for empty patterns or text", () => {
|
||||
expect(matchStreamErrorPatterns([], "x")).toBeNull();
|
||||
expect(matchStreamErrorPatterns(["x"], "")).toBeNull();
|
||||
expect(matchStreamErrorPatterns(null, "x")).toBeNull();
|
||||
expect(matchStreamErrorPatterns(undefined, "x")).toBeNull();
|
||||
});
|
||||
|
||||
it("regex matching is stateless across calls", () => {
|
||||
const pats = ["/error/i"];
|
||||
expect(matchStreamErrorPatterns(pats, "ERROR")).toBe("/error/i");
|
||||
expect(matchStreamErrorPatterns(pats, "ERROR")).toBe("/error/i");
|
||||
});
|
||||
|
||||
it("parsePatterns normalizes entries", () => {
|
||||
const parsed = parsePatterns(["Plain", "/re/g", "", "/bad["]);
|
||||
expect(parsed.length).toBe(3);
|
||||
expect(parsed[0]).toEqual({ text: "plain", raw: "Plain" });
|
||||
expect(parsed[1].regex).toBeInstanceOf(RegExp);
|
||||
expect(parsed[2]).toEqual({ text: "/bad[", raw: "/bad[" });
|
||||
});
|
||||
|
||||
it("skips entries that look like regex but fail to compile", () => {
|
||||
const parsed = parsePatterns(["/[unclosed/", "/ok/g"]);
|
||||
expect(parsed.length).toBe(1);
|
||||
expect(parsed[0].raw).toBe("/ok/g");
|
||||
});
|
||||
|
||||
it("streamStatusForContent maps match to error status", () => {
|
||||
expect(streamStatusForContent(["boom"], "a boom happened")).toBe("error");
|
||||
expect(streamStatusForContent(["boom"], "all good")).toBe("success");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user