/**
 * Shared retry helper for LLMClient adapters.
 *
 * Bounded exponential backoff with optional `Retry-After` honoring and
 * symmetric jitter. Treats 429 / 5xx as retryable and any other HTTP error
 * as terminal.
 *
 * Errors carry the full response body on the instance for callers that need
 * to inspect it; the message is intentionally short and body-free so it's
 * safe to include in user-facing logs and stack traces.
 */
export const DEFAULT_RETRY_POLICY = {
    maxAttempts: 3,
    baseDelayMs: 500,
    backoffFactor: 2,
    maxDelayMs: 10_000,
    jitter: 0.3,
};
export class LLMHttpError extends Error {
    status;
    attempts;
    /** Full upstream response body (kept on the instance, NOT in `message`). */
    body;
    constructor(status, body, attempts) {
        super(`LLM request failed with status ${status} after ${attempts} attempt(s)`);
        this.status = status;
        this.attempts = attempts;
        this.name = "LLMHttpError";
        this.body = body;
    }
}
/**
 * Sanitized error raised when an LLM adapter receives an HTTP-200 response
 * whose body is not valid JSON. The raw response body (which may echo back
 * user prompt content or even API-key fragments from prompts) is kept on the
 * instance for callers that opt in via `.raw`, NOT in the message string.
 *
 * Mirrors the LLMHttpError pattern verified by the
 * "does not leak the response body" test in openai-llm-client.test.ts.
 */
export class LLMParseError extends Error {
    /** Full raw response body (kept on the instance, NOT in `message`). */
    raw;
    /** Byte length of `raw` — safe to include in the message. */
    rawLength;
    constructor(message, raw, options) {
        super(`${message} (raw=${raw.length}B)`, options);
        this.name = "LLMParseError";
        this.raw = raw;
        this.rawLength = raw.length;
    }
}
export function isRetryableStatus(status) {
    return status === 429 || (status >= 500 && status < 600);
}
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function runWithRetry(args) {
    const { policy, attempt, sleep = defaultSleep, rng = Math.random } = args;
    for (let i = 1; i <= policy.maxAttempts; i++) {
        const res = await attempt();
        if (res.ok)
            return res.value;
        const canRetry = i < policy.maxAttempts && isRetryableStatus(res.status);
        if (!canRetry) {
            throw new LLMHttpError(res.status, res.body, i);
        }
        const exp = policy.baseDelayMs * Math.pow(policy.backoffFactor, i - 1);
        const base = res.retryAfterSeconds ? res.retryAfterSeconds * 1000 : exp;
        const capped = Math.min(base, policy.maxDelayMs);
        const jittered = policy.jitter > 0
            ? capped * (1 + (rng() - 0.5) * 2 * policy.jitter)
            : capped;
        await sleep(Math.max(0, Math.round(jittered)));
    }
    // Unreachable: the canRetry branch always throws on the final attempt.
    // Defensive throw so the type checker sees a definite return.
    throw new LLMHttpError(0, "no error body", policy.maxAttempts);
}
/** Parses a `Retry-After` header (seconds-only form). */
export function parseRetryAfterSeconds(header) {
    if (!header)
        return undefined;
    const n = Number(header);
    if (Number.isFinite(n) && n >= 0)
        return n;
    return undefined;
}
