From 80dee4d2c932c131e82e65910118553517270cb5 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 8 Mar 2026 02:14:33 +0900 Subject: [PATCH] fix(shared): add stable retry status dedupe helpers Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/shared/retry-status-utils.test.ts | 41 +++++++++++++++++++++ src/shared/retry-status-utils.ts | 51 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/shared/retry-status-utils.test.ts create mode 100644 src/shared/retry-status-utils.ts diff --git a/src/shared/retry-status-utils.test.ts b/src/shared/retry-status-utils.test.ts new file mode 100644 index 000000000..a9823b0b5 --- /dev/null +++ b/src/shared/retry-status-utils.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from "bun:test" + +import { extractRetryAttempt, extractRetryStatusModel, normalizeRetryStatusMessage } from "./retry-status-utils" + +describe("retry-status-utils", () => { + test("extracts retry attempt from explicit status attempt", () => { + //#given + const attempt = 6 + + //#when + const result = extractRetryAttempt(attempt, "The usage limit has been reached [retrying in 27s attempt #6]") + + //#then + expect(result).toBe(6) + }) + + test("extracts retry model from cooldown status text", () => { + //#given + const message = "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 56s attempt #1]" + + //#when + const result = extractRetryStatusModel(message) + + //#then + expect(result).toBe("claude-opus-4-6") + }) + + test("normalizes countdown jitter to a stable cooldown class", () => { + //#given + const firstMessage = "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 56s attempt #1]" + const secondMessage = "All credentials for model claude-opus-4-6 are cooling down [retrying in 7m 55s attempt #1]" + + //#when + const firstResult = normalizeRetryStatusMessage(firstMessage) + const secondResult = normalizeRetryStatusMessage(secondMessage) + + //#then + expect(firstResult).toBe("cooldown") + expect(secondResult).toBe("cooldown") + }) +}) diff --git a/src/shared/retry-status-utils.ts b/src/shared/retry-status-utils.ts new file mode 100644 index 000000000..c4514b1c8 --- /dev/null +++ b/src/shared/retry-status-utils.ts @@ -0,0 +1,51 @@ +const RETRY_COUNTDOWN_PATTERN = /\[\s*retrying\s+in[^\]]*\]/gi + +function collapseWhitespace(value: string): string { + return value.toLowerCase().replace(/\s+/g, " ").trim() +} + +export function extractRetryAttempt(attempt: number | undefined, message: string): number | "?" { + if (typeof attempt === "number" && Number.isFinite(attempt)) { + return attempt + } + + const parsedAttempt = message.match(/attempt\s*#\s*(\d+)/i)?.[1] + return parsedAttempt ? Number.parseInt(parsedAttempt, 10) : "?" +} + +export function extractRetryStatusModel(message: string): string | undefined { + return message.match(/model\s+([a-z0-9._/-]+)(?=\s+(?:are|is)\b)/i)?.[1]?.toLowerCase() +} + +export function normalizeRetryStatusMessage(message: string): string { + const normalizedMessage = collapseWhitespace(message.replace(RETRY_COUNTDOWN_PATTERN, " ")) + if (!normalizedMessage) { + return "retry" + } + + if (/all\s+credentials\s+for\s+model|cool(?:ing)?\s+down|cooldown|exhausted\s+your\s+capacity/.test(normalizedMessage)) { + return "cooldown" + } + + if (/too\s+many\s+requests/.test(normalizedMessage)) { + return "too-many-requests" + } + + if (/quota\s+will\s+reset\s+after|quota\s*exceeded/.test(normalizedMessage)) { + return "quota" + } + + if (/usage\s+limit\s+has\s+been\s+reached|limit\s+reached/.test(normalizedMessage)) { + return "usage-limit" + } + + if (/rate\s+limit/.test(normalizedMessage)) { + return "rate-limit" + } + + if (/service.?unavailable|temporarily.?unavailable|overloaded/.test(normalizedMessage)) { + return "service-unavailable" + } + + return normalizedMessage +}