6f76a6a9da
* fix(action): tighten provider error detection and propagate agent error events Both bugs from #562: 1. detectProviderError used substring matches against "429", "rate limit", etc. — false-positives on commit SHAs containing 429 and on x-ratelimit-* response headers in dumped 401 error JSON. rewrote with anchored regexes: numeric status codes only match adjacent to a recognised status key, and `\brate[_ ]limit(?=[_ ]|\b)` rejects ratelimit-* headers (no separator). word-boundary anchors on INTERNAL / UNAVAILABLE / quota / limit:0 reject INTERNAL_SERVER_ERROR / time_limit:0 substrings. added 11-case regression test. 2. opencode 401s slipped through `eventCount === 0 && lastProviderError` because opencode's own type=error event increments eventCount before the guard runs. added an explicit `error:` handler that captures the event and propagates it to a non-success AgentResult. opencode emits the message under `error.data.message`, not the top level. mirror fix in claude.ts: error_max_turns / error_during_execution / any error* subtype on the result event now flips success: false. * fix(action): match quota inside identifiers like insufficient_quota \bquota\b missed insufficient_quota / quota_exceeded / quotaExceeded because _ is a word character and camelCase has no boundary. quota is specific enough to be matched as a plain substring. * fix(action): match `rate limited` and `rate limits exceeded` Drop the trailing `(?=[_ ]|\b)` lookahead from the rate-limit regex. The lookahead failed when `limit` was followed by another word character (`limited`, `limits`), so `rate limited` and `rate limits exceeded` were slipping past detection. The leading `\b` plus `[_ ]` separator already rejects `x-ratelimit-*` / `anthropic-ratelimit-*` headers without it. --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com> Co-authored-by: David Blass <david@arktype.io>
39 lines
2.0 KiB
TypeScript
39 lines
2.0 KiB
TypeScript
type ProviderErrorPattern = { regex: RegExp; label: string };
|
|
|
|
// status codes are only treated as provider errors when they are adjacent to
|
|
// a recognised status key. this rejects commit SHAs that happen to contain
|
|
// "429", version strings, file hashes, etc.
|
|
const statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
|
|
const PROVIDER_ERROR_PATTERNS: ProviderErrorPattern[] = [
|
|
{ regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
|
|
{ regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
|
|
{ regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
|
|
// matches `rate limit`, `rate limited`, `rate limits exceeded`,
|
|
// `rate_limit_error`, `rate_limit_exceeded`. the leading `\b` + `[_ ]`
|
|
// separator rejects `x-ratelimit-*` / `anthropic-ratelimit-*` response
|
|
// headers (no separator between "rate" and "limit") which routinely
|
|
// appear in dumped 401 / 4xx error JSON.
|
|
{ regex: /\brate[_ ]limit/i, label: "rate limited" },
|
|
{ regex: /\bRESOURCE_EXHAUSTED\b/, label: "quota exhausted" },
|
|
// Google gRPC `INTERNAL` status. word-boundary anchors reject
|
|
// `INTERNAL_SERVER_ERROR` (HTTP 500 message that may appear in unrelated
|
|
// log lines) and identifiers like `INTERNALS`.
|
|
{ regex: /\bINTERNAL\b/, label: "provider internal error" },
|
|
{ regex: /\bUNAVAILABLE\b/, label: "provider unavailable" },
|
|
// matches `quota`, `insufficient_quota`, `quota_exceeded`, `quotaExceeded`.
|
|
// word-character lookarounds would reject `_quota` / `quotaX`; `quota` is
|
|
// specific enough that a plain substring match is safe.
|
|
{ regex: /quota/i, label: "quota error" },
|
|
// explicit zero-quota response, e.g. `{"limit": 0}`. the `\b` anchor
|
|
// around `limit` rejects keys like `time_limit` or `field_limit`.
|
|
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" },
|
|
];
|
|
|
|
export function detectProviderError(text: string): string | null {
|
|
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
if (entry.regex.test(text)) return entry.label;
|
|
}
|
|
return null;
|
|
}
|