Z ZPI 文档 v0.3.8
介绍页

源码

packages/ai/src/utils/model-retry.ts

typescript172 行查看原文
// Structured error details adapted from ZCode failure-classifier.ts (Apache-2.0).
// Retry decisions use Pi utils/retry.ts. See licenses/third-party-notices.txt.
import type { ModelFailure } from "../types.ts";

const TERMINAL_CODES = new Set([
  "MODEL_RECOVERY_DEADLINE",
  "1005",
  "1006",
  "1008",
  "1113",
  "1261",
  "1304",
  "1308",
  "1309",
  "1310",
  "1311",
  "1313",
  "1314",
  "1315",
  "1316",
  "1317",
  "1318",
  "1319",
  "1320",
  "1321",
  "20097",
  "2056",
  "3001",
  "3006",
  "3007",
  "3008",
  "3009",
  "3010",
  "insufficient_quota",
  "credit_balance_exhausted",
  "organization_spend_limit_exceeded",
  "project_spend_limit_exceeded",
  "organization_usage_limit_exceeded",
  "exceeded_current_quota_error",
]);
const TRANSIENT_CODES = new Set([
  "500",
  "1120",
  "1230",
  "1234",
  "1302",
  "1303",
  "1305",
  "1312",
  "2007",
  "3002",
  "rate_limit_reached_error",
  "rate_limit_error",
  "engine_overloaded_error",
  "overloaded_error",
  "network_error",
  "network_error_retryable",
  "ECONNRESET",
  "ECONNREFUSED",
  "EAI_AGAIN",
  "ENOTFOUND",
  "ENETUNREACH",
  "EHOSTUNREACH",
  "UND_ERR_SOCKET",
  "ETIMEDOUT",
  "ETIMEOUT",
  "UND_ERR_CONNECT_TIMEOUT",
  "UND_ERR_HEADERS_TIMEOUT",
  "UND_ERR_BODY_TIMEOUT",
  "MODEL_STREAM_IDLE_TIMEOUT",
  "MODEL_REQUEST_RETRY",
  "EMPTY_MODEL_RESPONSE",
]);

function record(value: unknown): Record<string, unknown> {
  return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : {};
}

/** Preserve retry facts before provider errors are converted into transcript text. */
export function modelFailure(error: unknown, signal?: AbortSignal): ModelFailure {
  const seen = new Set<unknown>();
  const records: Record<string, unknown>[] = [];
  const pending = [error];
  while (pending.length) {
    const current = pending.shift();
    if (!current || seen.has(current)) continue;
    seen.add(current);
    records.push(record(current));
    pending.push(record(current).cause, record(current).error);
  }
  const status = records.map((r) => r.status ?? r.statusCode).find((v) => typeof v === "number") as
    | number
    | undefined;
  const headersValue = records.map((r) => r.headers ?? r.responseHeaders).find((v) => v != null);
  const headers =
    headersValue instanceof Headers
      ? headersValue
      : new Headers(
          Object.entries(record(headersValue)).filter(
            (entry): entry is [string, string] => typeof entry[1] === "string",
          ),
        );
  const shouldRetry = headers.get("x-should-retry")?.trim().toLowerCase();
  let retryAfterMs: number | undefined;
  if (shouldRetry !== "false" && shouldRetry !== "0") {
    const ms = headers.get("retry-after-ms");
    const after = headers.get("retry-after");
    if (ms?.trim() && Number.isFinite(Number(ms))) retryAfterMs = Math.max(0, Math.round(Number(ms)));
    else if (after?.trim()) {
      const delay = Number.isFinite(Number(after)) ? Number(after) * 1000 : Date.parse(after) - Date.now();
      if (Number.isFinite(delay)) retryAfterMs = Math.max(0, Math.round(delay));
    }
  }
  const codes = records.flatMap((r) => {
    const body = record(r.error);
    return [r.code, body.code, body.type].flatMap((v) =>
      typeof v === "string" || typeof v === "number" ? [String(v)] : [],
    );
  });
  const aborted = signal?.aborted || records.some((r) => r.name === "AbortError" || r.code === "ABORT_ERR");
  const terminal =
    codes.some((code) => TERMINAL_CODES.has(code) || /CERT|TLS|SSL/iu.test(code)) ||
    records.some((r) => r.type === "InvalidInput" || r.isRetryable === false);
  const transient =
    codes.some((code) => TRANSIENT_CODES.has(code) || code.toUpperCase().includes("PROXY")) ||
    status === 408 ||
    status === 429 ||
    (status !== undefined && status >= 500) ||
    records.some(
      (r) =>
        r.isRetryable === true ||
        r.name === "APIConnectionError" ||
        r.name === "APIConnectionTimeoutError" ||
        r.name === "TimeoutError" ||
        r.name === "timeout_error" ||
        (record(r.error).type === "api_error" &&
          ["500 internal network error", "internal network error", "internal network failure"].includes(
            String(record(r.error).message ?? r.message ?? "")
              .trim()
              .toLowerCase(),
          )) ||
        (typeof r.message === "string" && /^Stream ended without /u.test(r.message)),
    ) ||
    (status === 409 && shouldRetry !== "false") ||
    shouldRetry === "true";
  const retryable =
    aborted || terminal || shouldRetry === "false" || shouldRetry === "0"
      ? false
      : transient
        ? true
        : status !== undefined
          ? false
          : undefined;
  const stall = records.find((r) => r.code === "MODEL_STREAM_IDLE_TIMEOUT");
  const recoveryCode = codes.find(
    (code) => code === "MODEL_REQUEST_RETRY" || code === "MODEL_RECOVERY_DEADLINE",
  );
  return {
    ...(retryable !== undefined ? { retryable } : {}),
    ...((recoveryCode ?? codes[0]) ? { code: recoveryCode ?? codes[0] } : {}),
    ...(stall
      ? {
          code: "MODEL_STREAM_IDLE_TIMEOUT",
          ...(typeof stall.idleMs === "number" ? { idleMs: stall.idleMs } : {}),
          ...(typeof stall.timeoutMs === "number" ? { timeoutMs: stall.timeoutMs } : {}),
        }
      : {}),
    ...(status !== undefined ? { status } : {}),
    ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
  };
}