action: center provider-error log excerpt on the matched line (closes #703)
the `» provider error detected (...)` excerpt was `chunk.substring(0, 500)`
— the head of whatever stderr buffer node delivered. on big writes that's
the front of an mcp tool-schema dump, not the matched error text. label
was correct (regex.test on the whole chunk), excerpt was misleading.
introduce findProviderErrorMatch(text) that returns { label, excerpt }
where excerpt is a windowed slice centered on the regex match index:
the matched line plus 1 line before and 2 lines after, hard-capped at
600 bytes. detectProviderError stays as a thin wrapper for label-only
callers. both opencode and claude harnesses log match.excerpt instead
of chunk.substring(0, 500).
regression tests cover the multi-line buffer case, surrounding-line
context, byte-cap fallback to matched-line-only, and head truncation
of a single oversize line.
This commit is contained in:
committed by
pullfrog[bot]
parent
8d6460da1c
commit
b9383bbcfd
+60
-2
@@ -41,13 +41,71 @@ const PROVIDER_ERROR_PATTERNS: ProviderErrorPattern[] = [
|
||||
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" },
|
||||
];
|
||||
|
||||
export function detectProviderError(text: string): string | null {
|
||||
/**
|
||||
* Result of a provider-error scan: the classification label plus a
|
||||
* human-readable excerpt centered on the matched line. The excerpt is what
|
||||
* gets surfaced in `» provider error detected (...)` log lines — see
|
||||
* `extractExcerpt` for the windowing/byte-cap policy.
|
||||
*/
|
||||
export type ProviderErrorMatch = {
|
||||
label: string;
|
||||
excerpt: string;
|
||||
};
|
||||
|
||||
// roughly half a wide terminal line by 4–5 lines of context; large enough
|
||||
// to capture a structured error payload (request id, retry-after, model)
|
||||
// plus its immediate stack/headers, small enough to not flood the log.
|
||||
const EXCERPT_MAX_BYTES = 600;
|
||||
const LINES_BEFORE = 1;
|
||||
const LINES_AFTER = 2;
|
||||
|
||||
export function findProviderErrorMatch(text: string): ProviderErrorMatch | null {
|
||||
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
||||
if (entry.regex.test(text)) return entry.label;
|
||||
const m = entry.regex.exec(text);
|
||||
if (!m) continue;
|
||||
return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function detectProviderError(text: string): string | null {
|
||||
return findProviderErrorMatch(text)?.label ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a context window around `matchIndex`: the matched line plus
|
||||
* `LINES_BEFORE`/`LINES_AFTER` neighbours. If the windowed slice exceeds
|
||||
* `EXCERPT_MAX_BYTES` (giant adjacent lines, e.g. JSON tool-schema dumps),
|
||||
* fall back to the matched line alone, head-truncated if still too long.
|
||||
* Replaces the old `chunk.substring(0, 500)` head-anchored excerpt which
|
||||
* surfaced whatever happened to be at the front of the stderr buffer
|
||||
* instead of the error itself. See issue #703.
|
||||
*/
|
||||
function extractExcerpt(text: string, matchIndex: number): string {
|
||||
const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
|
||||
const lineEndRaw = text.indexOf("\n", matchIndex);
|
||||
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
||||
|
||||
let start = lineStart;
|
||||
for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
|
||||
const prev = text.lastIndexOf("\n", start - 2);
|
||||
start = prev < 0 ? 0 : prev + 1;
|
||||
}
|
||||
|
||||
let end = lineEnd;
|
||||
for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
|
||||
const next = text.indexOf("\n", end + 1);
|
||||
end = next < 0 ? text.length : next;
|
||||
}
|
||||
|
||||
let excerpt = text.slice(start, end);
|
||||
if (excerpt.length > EXCERPT_MAX_BYTES) {
|
||||
excerpt = text.slice(lineStart, lineEnd);
|
||||
if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
|
||||
}
|
||||
return excerpt.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenRouter's response when the per-run key's remaining budget can't cover
|
||||
* the agent's `max_tokens` reservation. Distinct from a generic provider error
|
||||
|
||||
Reference in New Issue
Block a user