UNPKG

@blockrun/llm

Version:

TypeScript SDK for BlockRun - every frontier model behind one OpenAI-compatible client. Pay with a BlockRun API key or per-call in USDC. Smart routing picks the cheapest capable model. No rate limits.

14,870 lines • 545 kB
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/index.ts
var index_exports = {};
__export(index_exports, {
  APIError: () => APIError,
  API_KEY_URL: () => API_KEY_URL,
  ARC_CHAIN_ID: () => ARC_CHAIN_ID,
  AnthropicClient: () => AnthropicClient,
  BASE_CHAIN_ID: () => BASE_CHAIN_ID,
  BLOCKRUN_SOL_OPERATOR: () => BLOCKRUN_SOL_OPERATOR,
  BatchChannelLockedError: () => BatchChannelLockedError,
  BatchCloseDeferredError: () => BatchCloseDeferredError,
  BatchPaymentUnresolvedError: () => BatchPaymentUnresolvedError,
  BlockrunClient: () => BlockrunClient,
  BlockrunError: () => BlockrunError,
  DEFAULT_BATCH_IN_DOUBT: () => DEFAULT_BATCH_IN_DOUBT,
  DEFAULT_BATCH_LOCK_WAIT_MS: () => DEFAULT_BATCH_LOCK_WAIT_MS,
  DEFAULT_BATCH_RATE_LIMIT: () => DEFAULT_BATCH_RATE_LIMIT,
  EVM_NETWORKS: () => EVM_NETWORKS,
  ImageClient: () => ImageClient,
  KNOWN_PROVIDERS: () => KNOWN_PROVIDERS,
  LLMClient: () => LLMClient,
  MusicClient: () => MusicClient,
  NETWORK_ALIASES: () => NETWORK_ALIASES,
  OpenAI: () => OpenAI,
  PHONE_PRICES: () => PHONE_PRICES,
  PORTAL_URL: () => PORTAL_URL,
  PORTRAIT_ENROLLMENT_PRICE_USD: () => PORTRAIT_ENROLLMENT_PRICE_USD,
  PaymentError: () => PaymentError,
  PhoneClient: () => PhoneClient,
  PortraitClient: () => PortraitClient,
  PriceClient: () => PriceClient,
  RPC_PRICE_USD: () => RPC_PRICE_USD,
  RetiredEndpointError: () => RetiredEndpointError,
  RpcClient: () => RpcClient,
  SOLANA_NETWORK: () => SOLANA_NETWORK,
  SOLANA_WALLET_FILE_PATH: () => SOLANA_WALLET_FILE,
  SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
  SearchClient: () => SearchClient,
  SolanaBatchPayer: () => SolanaBatchPayer,
  SolanaLLMClient: () => SolanaLLMClient,
  SpeechClient: () => SpeechClient,
  SurfClient: () => SurfClient,
  USDC_ARC: () => USDC_ARC,
  USDC_BASE: () => USDC_BASE,
  USDC_BASE_CONTRACT: () => USDC_BASE_CONTRACT,
  USDC_SOLANA: () => USDC_SOLANA,
  VideoClient: () => VideoClient,
  VoiceClient: () => VoiceClient,
  WALLET_DIR_PATH: () => WALLET_DIR_PATH,
  WALLET_FILE_PATH: () => WALLET_FILE_PATH,
  clearCache: () => clearCache,
  createPaymentPayload: () => createPaymentPayload,
  createSolanaPaymentPayload: () => createSolanaPaymentPayload,
  createSolanaWallet: () => createSolanaWallet,
  createWallet: () => createWallet,
  default: () => client_default,
  evmNetwork: () => evmNetwork,
  extractPaymentDetails: () => extractPaymentDetails,
  formatFundingMessageCompact: () => formatFundingMessageCompact,
  formatNeedsFundingMessage: () => formatNeedsFundingMessage,
  formatSolanaWalletMigrationNotice: () => formatSolanaWalletMigrationNotice,
  formatWalletCreatedMessage: () => formatWalletCreatedMessage,
  formatWalletMigrationNotice: () => formatWalletMigrationNotice,
  getCached: () => getCached,
  getCachedByRequest: () => getCachedByRequest,
  getCostLogSummary: () => getCostLogSummary,
  getCostSummary: () => getCostSummary,
  getEip681Uri: () => getEip681Uri,
  getOrCreateSolanaWallet: () => getOrCreateSolanaWallet,
  getOrCreateWallet: () => getOrCreateWallet,
  getPaymentLinks: () => getPaymentLinks,
  getWalletAddress: () => getWalletAddress,
  importSolanaWallet: () => importSolanaWallet,
  importWallet: () => importWallet,
  listDiscoveredSolanaWallets: () => listDiscoveredSolanaWallets,
  listDiscoveredWallets: () => listDiscoveredWallets2,
  loadSolanaWallet: () => loadSolanaWallet,
  loadWallet: () => loadWallet,
  logCost: () => logCost,
  parsePaymentRequired: () => parsePaymentRequired,
  readSettlement: () => readSettlement,
  retryDisposition: () => retryDisposition,
  saveSolanaWallet: () => saveSolanaWallet,
  saveToCache: () => saveToCache,
  saveWallet: () => saveWallet,
  scanSolanaWallets: () => scanSolanaWallets,
  scanWallets: () => scanWallets2,
  setCache: () => setCache,
  setupAgentClient: () => setupAgentClient,
  setupAgentSolanaWallet: () => setupAgentSolanaWallet,
  setupAgentWallet: () => setupAgentWallet,
  solanaClient: () => solanaClient,
  solanaKeyToBytes: () => solanaKeyToBytes,
  solanaPublicKey: () => solanaPublicKey,
  status: () => status,
  toWireSearchParameters: () => toWireSearchParameters,
  toWireTools: () => toWireTools,
  validateMaxTokens: () => validateMaxTokens,
  validateModel: () => validateModel,
  validateTemperature: () => validateTemperature,
  validateTopP: () => validateTopP
});
module.exports = __toCommonJS(index_exports);

// src/types.ts
var RETRY_DISPOSITION = /* @__PURE__ */ Symbol.for("@blockrun/llm/retry-disposition/v1");
function retryDisposition(err) {
  if (!err || typeof err !== "object" && typeof err !== "function") return void 0;
  const value = err[RETRY_DISPOSITION];
  return value === "unpaid" || value === "paid-or-in-doubt" ? value : void 0;
}
function withDisposition(err, disposition) {
  if (!err || typeof err !== "object") return err;
  if (retryDisposition(err) === "paid-or-in-doubt") return err;
  try {
    Object.defineProperty(err, RETRY_DISPOSITION, { value: disposition, configurable: true, writable: true, enumerable: false });
  } catch {
  }
  return err;
}
var BlockrunError = class extends Error {
  constructor(message) {
    super(message);
    this.name = "BlockrunError";
  }
};
var RetiredEndpointError = class extends BlockrunError {
  constructor(message) {
    super(message);
    this.name = "RetiredEndpointError";
  }
};
var PaymentError = class extends BlockrunError {
  constructor(message) {
    super(message);
    this.name = "PaymentError";
  }
};
var APIError = class extends BlockrunError {
  statusCode;
  /** Retry-After header returned by the account API. */
  retryAfter;
  response;
  constructor(message, statusCode, response) {
    super(message);
    this.name = "APIError";
    this.statusCode = statusCode;
    this.response = response;
  }
};

// src/validation.ts
var LOCALHOST_DOMAINS = ["localhost", "127.0.0.1"];
var KNOWN_PROVIDERS = /* @__PURE__ */ new Set([
  "openai",
  "anthropic",
  "google",
  "deepseek",
  "mistralai",
  "meta-llama",
  "together",
  "xai",
  "moonshot",
  "nvidia",
  "minimax",
  "zai"
]);
function validateModel(model) {
  if (!model || typeof model !== "string") {
    throw new Error("Model must be a non-empty string");
  }
}
function validateMaxTokens(maxTokens) {
  if (maxTokens === void 0 || maxTokens === null) {
    return;
  }
  if (typeof maxTokens !== "number" || !Number.isInteger(maxTokens)) {
    throw new Error("maxTokens must be an integer");
  }
  if (maxTokens < 1) {
    throw new Error("maxTokens must be positive (minimum: 1)");
  }
}
function validateTemperature(temperature) {
  if (temperature === void 0 || temperature === null) {
    return;
  }
  if (typeof temperature !== "number") {
    throw new Error("temperature must be a number");
  }
  if (temperature < 0 || temperature > 2) {
    throw new Error("temperature must be between 0 and 2");
  }
}
function validateTopP(topP) {
  if (topP === void 0 || topP === null) {
    return;
  }
  if (typeof topP !== "number") {
    throw new Error("topP must be a number");
  }
  if (topP < 0 || topP > 1) {
    throw new Error("topP must be between 0 and 1");
  }
}
function validatePrivateKey(key) {
  if (typeof key !== "string") {
    throw new Error("Private key must be a string");
  }
  if (!key.startsWith("0x")) {
    throw new Error("Private key must start with 0x");
  }
  if (key.length !== 66) {
    throw new Error(
      "Private key must be 66 characters (0x + 64 hexadecimal characters)"
    );
  }
  if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
    throw new Error(
      "Private key must contain only hexadecimal characters (0-9, a-f, A-F)"
    );
  }
}
function validateApiUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error("Invalid API URL format");
  }
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    throw new Error(
      `Invalid protocol: ${parsed.protocol}. Use http:// or https://`
    );
  }
  const isLocalhost = LOCALHOST_DOMAINS.includes(parsed.hostname);
  if (parsed.protocol !== "https:" && !isLocalhost) {
    throw new Error(
      `API URL must use HTTPS for non-localhost endpoints. Use https:// instead of ${parsed.protocol}//`
    );
  }
}
function sanitizeErrorResponse(errorBody) {
  if (typeof errorBody !== "object" || errorBody === null) {
    return { message: "API request failed" };
  }
  const body = errorBody;
  const detail = typeof body.message === "string" && body.message !== body.error ? body.message : void 0;
  return {
    message: typeof body.error === "string" ? body.error : "API request failed",
    code: typeof body.code === "string" ? body.code : void 0,
    ...detail ? { detail } : {},
    ...typeof body.hint === "string" ? { hint: body.hint } : {}
  };
}
function validateResourceUrl(url, baseUrl) {
  try {
    const parsed = new URL(url);
    const baseParsed = new URL(baseUrl);
    if (parsed.hostname !== baseParsed.hostname) {
      console.warn(
        `Resource URL hostname mismatch: ${parsed.hostname} vs ${baseParsed.hostname}. Using safe default instead.`
      );
      return `${baseUrl}/v1/chat/completions`;
    }
    if (parsed.protocol !== baseParsed.protocol) {
      console.warn(
        `Resource URL protocol mismatch: ${parsed.protocol} vs ${baseParsed.protocol}. Using safe default instead.`
      );
      return `${baseUrl}/v1/chat/completions`;
    }
    return url;
  } catch {
    console.warn(`Invalid resource URL format: ${url}. Using safe default.`);
    return `${baseUrl}/v1/chat/completions`;
  }
}

// src/api-key.ts
var TRANSIENT_STATUS = /* @__PURE__ */ new Set([502, 503, 504, 522, 524]);
var TRANSIENT_RETRIES = 2;
var TRANSIENT_BACKOFF_MS = 1e3;
function retryDelay(ms, signal) {
  signal?.throwIfAborted();
  return new Promise((resolve2, reject) => {
    const onAbort = () => {
      clearTimeout(timer);
      signal?.removeEventListener("abort", onAbort);
      reject(signal?.reason);
    };
    const timer = setTimeout(() => {
      signal?.removeEventListener("abort", onAbort);
      resolve2();
    }, ms);
    signal?.addEventListener("abort", onAbort, { once: true });
  });
}
var API_KEY_URL = "https://api.blockrun.ai";
var PORTAL_URL = "https://user.blockrun.ai";
function resolveApiKeyAuth(options) {
  if (options.apiKey !== void 0 && options.privateKey !== void 0) {
    throw new Error("Pass either apiKey or privateKey, not both.");
  }
  const key = options.apiKey ?? (options.privateKey === void 0 && typeof process !== "undefined" ? process.env?.BLOCKRUN_API_KEY : void 0);
  if (key === void 0) return void 0;
  if (!/^brk_[A-Za-z0-9_-]+$/.test(key.trim())) {
    throw new Error(`Invalid BlockRun API key. Create one at ${PORTAL_URL}/dashboard/keys.`);
  }
  const base = options.apiUrl ?? (typeof process !== "undefined" ? process.env?.BLOCKRUN_API_BASE_URL : void 0) ?? API_KEY_URL;
  return new ApiKeyAuth(key.trim(), base);
}
function accountErrorDisposition(err) {
  if (!(err instanceof APIError)) return "paid-or-in-doubt";
  if (err.statusCode >= 400 && err.statusCode < 500) return "unpaid";
  if (err.statusCode >= 500 && hasGatewayErrorEnvelope(err.response)) return "unpaid";
  return "paid-or-in-doubt";
}
function hasGatewayErrorEnvelope(response) {
  return !!response && typeof response === "object" && typeof response.message === "string" && response.message.length > 0;
}
function requireWallet(value) {
  if (value === void 0) throw new Error("This operation requires a wallet; the client uses API key account billing.");
  return value;
}
var ApiKeyAuth = class {
  apiUrl;
  #key;
  constructor(key, base) {
    validateApiUrl(base);
    const url = new URL(base);
    if (url.username || url.password || url.search || url.hash) {
      throw new Error("API base URL must not contain credentials, a query, or a fragment.");
    }
    this.apiUrl = base.replace(/\/+$/, "").replace(/\/v1$/, "");
    this.#key = key;
  }
  /** Resolve gateway poll paths without forwarding credentials to another origin. */
  resolveUrl(path7) {
    const url = new URL(path7, this.apiUrl + "/");
    if (url.origin !== new URL(this.apiUrl).origin || url.username || url.password) {
      throw new Error("Refusing to send a BlockRun API key to a different origin.");
    }
    if (new URL(this.apiUrl).pathname === "/" && url.pathname.startsWith("/api/v1/")) url.pathname = url.pathname.slice(4);
    return url.href;
  }
  async fetch(input, init, raiseErrors = true) {
    const request = input instanceof Request ? input : void 0;
    const url = this.resolveUrl(request?.url ?? String(input));
    const headers = new Headers(init?.headers ?? request?.headers);
    for (const name of [...headers.keys()]) {
      if (name.toLowerCase().includes("payment") || name.toLowerCase() === "x-api-key") headers.delete(name);
    }
    headers.set("authorization", `Bearer ${this.#key}`);
    const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
    const retries = method === "GET" || method === "HEAD" ? TRANSIENT_RETRIES : 0;
    const signal = init?.signal ?? request?.signal;
    let response;
    for (let attempt = 0; ; attempt++) {
      response = await globalThis.fetch(request ? new Request(url, request) : url, {
        ...init,
        headers,
        redirect: "error"
      });
      if (attempt >= retries || !TRANSIENT_STATUS.has(response.status)) break;
      await response.body?.cancel();
      await retryDelay(TRANSIENT_BACKOFF_MS * (attempt + 1), signal);
    }
    if (raiseErrors && !response.ok) {
      let body;
      try {
        body = await response.json();
      } catch {
        body = void 0;
      }
      const outer = body && typeof body === "object" ? body : {};
      const detail = outer.error && typeof outer.error === "object" ? outer.error : outer;
      const safe = Object.fromEntries(["message", "code", "type", "param"].flatMap((name) => typeof detail[name] === "string" ? [[name, detail[name].split(this.#key).join("[REDACTED]")]] : []));
      const hint = response.status === 402 ? ` Top up at ${PORTAL_URL}/dashboard/credits.` : response.status === 401 ? ` Check your key at ${PORTAL_URL}/dashboard/keys.` : "";
      const error = new APIError(`BlockRun account API error: ${response.status}.${hint}`, response.status, safe);
      error.retryAfter = response.headers.get("retry-after") ?? void 0;
      throw error;
    }
    return response;
  }
  /** Poll an account-owned async job without ever entering the x402 signing path. */
  async poll(response, timeout, interval) {
    const deadline = Date.now() + timeout;
    let data = await response.json();
    if (["failed", "cancelled", "canceled"].includes(String(data.status))) {
      throw new APIError("Account API job failed or was cancelled", 502);
    }
    const pollPath = data.poll_url;
    if (typeof pollPath !== "string") {
      if (response.status === 202 || ["queued", "in_progress", "processing"].includes(String(data.status))) {
        throw new APIError("Async response missing poll_url", response.status);
      }
      return data;
    }
    const url = this.resolveUrl(pollPath);
    while (Date.now() < deadline) {
      if (data.status === "completed") return data;
      if (["failed", "cancelled", "canceled"].includes(String(data.status))) {
        throw new APIError("Account API job failed or was cancelled", 502, sanitizeErrorResponse(data));
      }
      await new Promise((resolve2) => setTimeout(resolve2, Math.min(interval, Math.max(0, deadline - Date.now()))));
      const remaining = deadline - Date.now();
      if (remaining <= 0) break;
      try {
        const res = await this.fetch(url, { signal: AbortSignal.timeout(remaining) });
        data = await res.json();
      } catch (err) {
        if (!(err instanceof APIError) || !TRANSIENT_STATUS.has(err.statusCode)) throw err;
      }
    }
    if (data.status === "completed") return data;
    throw new APIError("Account API job polling timed out; check the job before submitting again.", 504, { poll_url: url });
  }
};

// src/search-wire.ts
function put(out, key, value) {
  if (value !== void 0) out[key] = value;
}
function handles(list) {
  return list?.map((h) => h.trim().replace(/^@+/, ""));
}
function sourceToWire(source) {
  const raw = source;
  const out = { ...raw };
  for (const key of Object.keys(out)) if (/[A-Z]/.test(key)) delete out[key];
  switch (source.type) {
    case "x":
      put(out, "included_x_handles", handles(source.includedXHandles) ?? out.included_x_handles);
      put(out, "excluded_x_handles", handles(source.excludedXHandles) ?? out.excluded_x_handles);
      put(out, "post_favorite_count", source.postFavoriteCount);
      put(out, "post_view_count", source.postViewCount);
      put(out, "enable_image_understanding", source.enableImageUnderstanding);
      put(out, "enable_video_understanding", source.enableVideoUnderstanding);
      break;
    case "web":
      put(out, "country", source.country);
      put(out, "allowed_websites", source.allowedWebsites);
      put(out, "excluded_websites", source.excludedWebsites);
      put(out, "safe_search", source.safeSearch);
      put(out, "enable_image_understanding", source.enableImageUnderstanding);
      break;
    case "news":
      put(out, "country", source.country);
      put(out, "allowed_websites", source.allowedWebsites);
      put(out, "excluded_websites", source.excludedWebsites);
      put(out, "safe_search", source.safeSearch);
      break;
    case "rss":
      put(out, "links", source.links);
      break;
  }
  return out;
}
function toWireSearchParameters(sp) {
  const raw = sp;
  const out = {};
  for (const [key, value] of Object.entries(raw)) if (!/[A-Z]/.test(key) && key !== "sources") out[key] = value;
  put(out, "mode", sp.mode);
  if (sp.sources) out.sources = sp.sources.map(sourceToWire);
  put(out, "return_citations", sp.returnCitations);
  put(out, "from_date", sp.fromDate);
  put(out, "to_date", sp.toDate);
  put(out, "max_search_results", sp.maxSearchResults);
  put(out, "max_turns", sp.maxTurns);
  return out;
}
function toWireTools(tools) {
  return tools.map((tool) => {
    if (tool.type === "x_search") {
      const out = { type: "x_search" };
      put(out, "allowed_x_handles", handles(tool.allowedXHandles));
      put(out, "excluded_x_handles", handles(tool.excludedXHandles));
      put(out, "from_date", tool.fromDate);
      put(out, "to_date", tool.toDate);
      put(out, "enable_image_understanding", tool.enableImageUnderstanding);
      put(out, "enable_video_understanding", tool.enableVideoUnderstanding);
      return out;
    }
    if (tool.type === "web_search") {
      const out = { type: "web_search" };
      put(out, "allowed_domains", tool.allowedDomains);
      put(out, "excluded_domains", tool.excludedDomains);
      put(out, "enable_image_understanding", tool.enableImageUnderstanding);
      return out;
    }
    return tool;
  });
}

// src/receipt.ts
function atomicToUsd(amount) {
  return typeof amount === "string" && /^\d+$/.test(amount) ? Number(amount) / 1e6 : void 0;
}
function receiptChargedUsd(receipt) {
  return atomicToUsd(receipt?.extra?.chargedAmount) ?? atomicToUsd(receipt?.amount);
}
function decode(header) {
  try {
    const json = typeof Buffer !== "undefined" ? Buffer.from(header, "base64").toString("utf8") : atob(header);
    const parsed = JSON.parse(json);
    return parsed && typeof parsed === "object" ? parsed : void 0;
  } catch {
    return void 0;
  }
}
function readSettlement(response, quotedUsd, scheme) {
  const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
  const receipt = header ? decode(header) : void 0;
  const extra = receipt?.extra;
  const costUsd = receiptChargedUsd({
    amount: receipt?.amount,
    extra: extra && typeof extra === "object" ? extra : void 0
  }) ?? quotedUsd;
  const receiptTx = typeof receipt?.transaction === "string" && receipt.transaction ? receipt.transaction : void 0;
  const transaction = receiptTx ?? (response.headers.get("X-Payment-Receipt")?.trim() || void 0);
  const settlement = {
    scheme,
    quotedUsd,
    ...transaction ? { transaction } : {},
    ...typeof receipt?.network === "string" ? { network: receipt.network } : {},
    ...receipt?.success === false ? { success: false } : {}
  };
  return { costUsd, settlement };
}
function settlementSkipReason(response) {
  const headers = response.headers;
  if (typeof headers?.get !== "function") return void 0;
  const reason = [headers.get("X-Settlement-Skipped"), headers.get("X-Payment-Waived")].map((value) => value?.trim()).find((value) => !!value && !NOT_A_SKIP.test(value));
  if (reason === void 0) return void 0;
  const evidence = settlementEvidence(response);
  if (evidence !== void 0) {
    console.error(
      `[@blockrun/llm] settlement: the gateway named a skipped settlement (${JSON.stringify(reason)}) on a response that also carries ${evidence}; booked as charged`
    );
    return void 0;
  }
  return reason;
}
var NOT_A_SKIP = /^(false|0|no|none)$/i;
function settlementEvidence(response) {
  const header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
  const receipt = header ? decode(header) : void 0;
  if (receipt && receipt.success !== false) {
    if (typeof receipt.transaction === "string" && receipt.transaction) return "a settlement receipt with a transaction";
    const extra = receipt.extra;
    const charged = receiptChargedUsd({
      amount: receipt.amount,
      extra: extra && typeof extra === "object" ? extra : void 0
    });
    if (charged !== void 0 && charged > 0) return "a settlement receipt with a charge";
  }
  if (response.headers.get("X-Payment-Receipt")?.trim()) return "an X-Payment-Receipt transaction";
  return void 0;
}
function settlementSkipped(response) {
  return settlementSkipReason(response) !== void 0;
}
function withCost(body, response, bookedUsd, scheme) {
  const skipped = settlementSkipReason(response);
  if (skipped !== void 0) {
    body.costUsd = 0;
    body.settlement = { ...readSettlement(response, bookedUsd, scheme).settlement, skipped };
    return body;
  }
  body.costUsd = bookedUsd;
  body.settlement = readSettlement(response, bookedUsd, scheme).settlement;
  return body;
}

// src/client.ts
var import_accounts2 = require("viem/accounts");

// node_modules/.pnpm/@blockrun+router-core@https+++codeload.github.com+BlockRunAI+router-core+tar.gz+fd6d03d035ff6_meidvh7c7i3y7wjjl6r2ymc4py/node_modules/@blockrun/router-core/dist/index.js
var UNSEGMENTED_SCRIPT = /[؀-ۿ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/;
var WORD_CHAR = /[\p{L}\p{N}]/u;
var ASCII_PHRASE = /^[a-z0-9' -]+$/;
var SEGMENTED_WORD_CHAR = "\\p{Script=Latin}\\p{Script=Cyrillic}\\p{Script=Greek}\\p{N}";
var BOUNDARY_BEFORE = `(?<![${SEGMENTED_WORD_CHAR}])`;
var BOUNDARY_AFTER = `(?![${SEGMENTED_WORD_CHAR}])`;
var BOUNDED_LAST_WORD_LENGTH = 6;
var SHORT_SUFFIXES = "(?:s|ed|ing)?";
var SIBILANT_PLURAL = "es";
var MID_SUFFIXES = "(?:s|es|ed|d|ing|er|ers|ly|ment|ments|ation|ations|e|en|n|a|as|o|os|em|et|ung|ungen)?";
var DROPPED_E_SUFFIXES = "(?:ing|ed|es|er|ers|ation|ations)";
var DOUBLED_CONSONANT_SUFFIXES = "(?:ing|ed|er|ers)";
var matcherCache = /* @__PURE__ */ new Map();
function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function keywordMatcher(keyword) {
  const cached = matcherCache.get(keyword);
  if (cached) return cached;
  const kw = keyword.toLowerCase();
  let matcher;
  if (UNSEGMENTED_SCRIPT.test(kw) || kw.length === 0) {
    matcher = { needle: kw, pattern: null };
  } else {
    const lead = WORD_CHAR.test(kw[0]) ? BOUNDARY_BEFORE : "";
    const escaped = escapeRegExp(kw);
    let body = escaped;
    let trail = "";
    let needle = kw;
    const lastWord = kw.slice(kw.lastIndexOf(" ") + 1);
    if (WORD_CHAR.test(kw[kw.length - 1]) && ASCII_PHRASE.test(kw) && lastWord.length <= BOUNDED_LAST_WORD_LENGTH) {
      const forms = [];
      if (/\d$/.test(kw)) {
        forms.push(escaped);
      } else if (lastWord.length <= 3) {
        forms.push(`${escaped}${SHORT_SUFFIXES}`);
        if (/(?:x|s|z|ch|sh)$/.test(kw)) forms.push(`${escaped}${SIBILANT_PLURAL}`);
      } else {
        forms.push(`${escaped}${MID_SUFFIXES}`);
        if (kw.endsWith("e")) {
          forms.push(`${escaped.slice(0, -1)}${DROPPED_E_SUFFIXES}`);
          needle = kw.slice(0, -1);
        } else if (kw.endsWith("y")) {
          forms.push(`${escaped.slice(0, -1)}(?:ies|ied)`);
          needle = kw.slice(0, -1);
        } else if (/[aeiou][bdgmnprt]$/.test(kw)) {
          forms.push(`${escaped}${kw[kw.length - 1]}${DOUBLED_CONSONANT_SUFFIXES}`);
        }
      }
      body = `(?:${forms.join("|")})`;
      trail = BOUNDARY_AFTER;
    }
    matcher = { needle, pattern: new RegExp(`${lead}${body}${trail}`, "u") };
  }
  matcherCache.set(keyword, matcher);
  return matcher;
}
function matchesKeyword(text, keyword) {
  const { needle, pattern } = keywordMatcher(keyword);
  if (!text.includes(needle)) return false;
  return pattern === null ? true : pattern.test(text);
}
function scoreTokenCount(estimatedTokens, thresholds) {
  if (estimatedTokens < thresholds.simple) {
    return { name: "tokenCount", score: -1, signal: `short (${estimatedTokens} tokens)` };
  }
  if (estimatedTokens > thresholds.complex) {
    return { name: "tokenCount", score: 1, signal: `long (${estimatedTokens} tokens)` };
  }
  return { name: "tokenCount", score: 0, signal: null };
}
function scoreKeywordMatch(text, keywords, name, signalLabel, thresholds, scores) {
  const matches = keywords.filter((kw) => matchesKeyword(text, kw));
  if (matches.length >= thresholds.high) {
    return {
      name,
      score: scores.high,
      signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
    };
  }
  if (matches.length >= thresholds.low) {
    return {
      name,
      score: scores.low,
      signal: `${signalLabel} (${matches.slice(0, 3).join(", ")})`
    };
  }
  return { name, score: scores.none, signal: null };
}
function scoreMultiStep(text) {
  const patterns = [/first.*then/i, /step \d/i, /\d\.\s/];
  const hits = patterns.filter((p) => p.test(text));
  if (hits.length > 0) {
    return { name: "multiStepPatterns", score: 0.5, signal: "multi-step" };
  }
  return { name: "multiStepPatterns", score: 0, signal: null };
}
function scoreQuestionComplexity(prompt) {
  const count = (prompt.match(/\?/g) || []).length;
  if (count > 3) {
    return { name: "questionComplexity", score: 0.5, signal: `${count} questions` };
  }
  return { name: "questionComplexity", score: 0, signal: null };
}
function scoreAgenticTask(text, keywords) {
  let matchCount = 0;
  const signals = [];
  for (const keyword of keywords) {
    if (matchesKeyword(text, keyword)) {
      matchCount++;
      if (signals.length < 3) {
        signals.push(keyword);
      }
    }
  }
  if (matchCount >= 4) {
    return {
      dimensionScore: {
        name: "agenticTask",
        score: 1,
        signal: `agentic (${signals.join(", ")})`
      },
      agenticScore: 1
    };
  } else if (matchCount >= 3) {
    return {
      dimensionScore: {
        name: "agenticTask",
        score: 0.6,
        signal: `agentic (${signals.join(", ")})`
      },
      agenticScore: 0.6
    };
  } else if (matchCount >= 1) {
    return {
      dimensionScore: {
        name: "agenticTask",
        score: 0.2,
        signal: `agentic-light (${signals.join(", ")})`
      },
      agenticScore: 0.2
    };
  }
  return {
    dimensionScore: { name: "agenticTask", score: 0, signal: null },
    agenticScore: 0
  };
}
function classifyByRules(prompt, systemPrompt, estimatedTokens, config) {
  const userText = prompt.toLowerCase();
  const dimensions = [
    // Token count uses total estimated tokens (system + user) — context size matters for model selection
    scoreTokenCount(estimatedTokens, config.tokenCountThresholds),
    scoreKeywordMatch(
      userText,
      config.codeKeywords,
      "codePresence",
      "code",
      { low: 1, high: 2 },
      { none: 0, low: 0.5, high: 1 }
    ),
    scoreKeywordMatch(
      userText,
      config.reasoningKeywords,
      "reasoningMarkers",
      "reasoning",
      { low: 1, high: 2 },
      { none: 0, low: 0.7, high: 1 }
    ),
    scoreKeywordMatch(
      userText,
      config.technicalKeywords,
      "technicalTerms",
      "technical",
      { low: 2, high: 4 },
      { none: 0, low: 0.5, high: 1 }
    ),
    scoreKeywordMatch(
      userText,
      config.creativeKeywords,
      "creativeMarkers",
      "creative",
      { low: 1, high: 2 },
      { none: 0, low: 0.5, high: 0.7 }
    ),
    scoreKeywordMatch(
      userText,
      config.simpleKeywords,
      "simpleIndicators",
      "simple",
      { low: 1, high: 2 },
      { none: 0, low: -1, high: -1 }
    ),
    scoreMultiStep(userText),
    scoreQuestionComplexity(prompt),
    // 6 new dimensions
    scoreKeywordMatch(
      userText,
      config.imperativeVerbs,
      "imperativeVerbs",
      "imperative",
      { low: 1, high: 2 },
      { none: 0, low: 0.3, high: 0.5 }
    ),
    scoreKeywordMatch(
      userText,
      config.constraintIndicators,
      "constraintCount",
      "constraints",
      { low: 1, high: 3 },
      { none: 0, low: 0.3, high: 0.7 }
    ),
    scoreKeywordMatch(
      userText,
      config.outputFormatKeywords,
      "outputFormat",
      "format",
      { low: 1, high: 2 },
      { none: 0, low: 0.4, high: 0.7 }
    ),
    scoreKeywordMatch(
      userText,
      config.referenceKeywords,
      "referenceComplexity",
      "references",
      { low: 1, high: 2 },
      { none: 0, low: 0.3, high: 0.5 }
    ),
    scoreKeywordMatch(
      userText,
      config.negationKeywords,
      "negationComplexity",
      "negation",
      { low: 2, high: 3 },
      { none: 0, low: 0.3, high: 0.5 }
    ),
    scoreKeywordMatch(
      userText,
      config.domainSpecificKeywords,
      "domainSpecificity",
      "domain-specific",
      { low: 1, high: 2 },
      { none: 0, low: 0.5, high: 0.8 }
    )
  ];
  const agenticResult = scoreAgenticTask(userText, config.agenticTaskKeywords);
  dimensions.push(agenticResult.dimensionScore);
  const agenticScore = agenticResult.agenticScore;
  const signals = dimensions.filter((d) => d.signal !== null).map((d) => d.signal);
  const weights = config.dimensionWeights;
  let weightedScore = 0;
  for (const d of dimensions) {
    const w = weights[d.name] ?? 0;
    weightedScore += d.score * w;
  }
  const reasoningMatches = config.reasoningKeywords.filter((kw) => matchesKeyword(userText, kw));
  if (reasoningMatches.length >= 2) {
    const confidence2 = calibrateConfidence(
      Math.max(weightedScore, 0.3),
      // ensure positive for confidence calc
      config.confidenceSteepness
    );
    return {
      score: weightedScore,
      tier: "REASONING",
      confidence: Math.max(confidence2, 0.85),
      signals,
      agenticScore,
      dimensions
    };
  }
  const { simpleMedium, mediumComplex, complexReasoning } = config.tierBoundaries;
  let tier;
  let distanceFromBoundary;
  if (weightedScore < simpleMedium) {
    tier = "SIMPLE";
    distanceFromBoundary = simpleMedium - weightedScore;
  } else if (weightedScore < mediumComplex) {
    tier = "MEDIUM";
    distanceFromBoundary = Math.min(weightedScore - simpleMedium, mediumComplex - weightedScore);
  } else if (weightedScore < complexReasoning) {
    tier = "COMPLEX";
    distanceFromBoundary = Math.min(
      weightedScore - mediumComplex,
      complexReasoning - weightedScore
    );
  } else {
    tier = "REASONING";
    distanceFromBoundary = weightedScore - complexReasoning;
  }
  const confidence = calibrateConfidence(distanceFromBoundary, config.confidenceSteepness);
  if (confidence < config.confidenceThreshold) {
    return { score: weightedScore, tier: null, confidence, signals, agenticScore, dimensions };
  }
  return { score: weightedScore, tier, confidence, signals, agenticScore, dimensions };
}
function calibrateConfidence(distance, steepness) {
  return 1 / (1 + Math.exp(-steepness * distance));
}
var BASELINE_MODEL_ID = "anthropic/claude-opus-4.7";
var BASELINE_INPUT_PRICE = 5;
var BASELINE_OUTPUT_PRICE = 25;
function selectModel(tier, confidence, method, reasoning, tierConfigs, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile, agenticScore) {
  const tierConfig = tierConfigs[tier];
  const model = tierConfig.primary;
  const pricing = modelPricing.get(model);
  let costEstimate;
  if (pricing?.flatPrice !== void 0) {
    costEstimate = pricing.flatPrice;
  } else {
    const inputPrice = pricing?.inputPrice ?? 0;
    const outputPrice = pricing?.outputPrice ?? 0;
    costEstimate = estimatedInputTokens / 1e6 * inputPrice + maxOutputTokens / 1e6 * outputPrice;
  }
  const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
  const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
  const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
  const baselineCost = baselineInput + baselineOutput;
  const savings = routingProfile === "premium" || pricing === void 0 ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
  return {
    model,
    tier,
    confidence,
    method,
    reasoning,
    costEstimate,
    baselineCost,
    savings,
    ...agenticScore !== void 0 && { agenticScore }
  };
}
function getFallbackChain(tier, tierConfigs) {
  const config = tierConfigs[tier];
  return [config.primary, ...config.fallback];
}
var SERVER_MARGIN_PERCENT = 5;
var MIN_PAYMENT_USD = 1e-3;
function calculateModelCost(model, modelPricing, estimatedInputTokens, maxOutputTokens, routingProfile) {
  const pricing = modelPricing.get(model);
  let costEstimate;
  if (pricing?.flatPrice !== void 0) {
    costEstimate = Math.max(pricing.flatPrice * (1 + SERVER_MARGIN_PERCENT / 100), MIN_PAYMENT_USD);
  } else {
    const inputPrice = pricing?.inputPrice ?? 0;
    const outputPrice = pricing?.outputPrice ?? 0;
    const inputCost = estimatedInputTokens / 1e6 * inputPrice;
    const outputCost = maxOutputTokens / 1e6 * outputPrice;
    costEstimate = Math.max(
      (inputCost + outputCost) * (1 + SERVER_MARGIN_PERCENT / 100),
      MIN_PAYMENT_USD
    );
  }
  const opusPricing = modelPricing.get(BASELINE_MODEL_ID);
  const opusInputPrice = opusPricing?.inputPrice ?? BASELINE_INPUT_PRICE;
  const opusOutputPrice = opusPricing?.outputPrice ?? BASELINE_OUTPUT_PRICE;
  const baselineInput = estimatedInputTokens / 1e6 * opusInputPrice;
  const baselineOutput = maxOutputTokens / 1e6 * opusOutputPrice;
  const baselineCost = baselineInput + baselineOutput;
  const savings = routingProfile === "premium" ? 0 : baselineCost > 0 ? Math.max(0, (baselineCost - costEstimate) / baselineCost) : 0;
  return { costEstimate, baselineCost, savings };
}
function filterCandidatesByCapacity(models, estimatedInputTokens, requestedOutputTokens, getCapabilities) {
  const filtered = models.filter((modelId) => {
    const capabilities = getCapabilities(modelId);
    if (!capabilities) return true;
    return capabilities.contextWindow >= (estimatedInputTokens + requestedOutputTokens) * 1.1 && capabilities.maxOutput >= requestedOutputTokens;
  });
  return filtered;
}
var CJK_CHARS = /[ -ヿ㐀-䶿一-鿿가-힯豈-﫿＀-￯]/g;
function estimateTokens(text) {
  if (text.length === 0) return 0;
  const cjk = text.match(CJK_CHARS)?.length ?? 0;
  return cjk + Math.ceil((text.length - cjk) / 4);
}
function scanLimit(promptTruncationChars) {
  return Math.max(1, Math.min(8e3, promptTruncationChars));
}
function sampleForScan(value, limit) {
  if (value.length <= limit) return value;
  const prefixLength = Math.ceil(limit / 2);
  return `${value.slice(0, prefixLength)}
${value.slice(-(limit - prefixLength))}`;
}
function applyPromotions(tierConfigs, promotions, profile, now = /* @__PURE__ */ new Date()) {
  if (!promotions || promotions.length === 0) return tierConfigs;
  let result = tierConfigs;
  for (const promo of promotions) {
    const start = new Date(promo.startDate);
    const end = new Date(promo.endDate);
    if (now < start || now >= end) continue;
    if (promo.profiles && !promo.profiles.includes(profile)) continue;
    if (result === tierConfigs) {
      result = { ...tierConfigs };
      for (const t of Object.keys(result)) {
        result[t] = { ...result[t] };
      }
    }
    for (const [tier, override] of Object.entries(promo.tierOverrides)) {
      if (!result[tier]) continue;
      if (override.primary) result[tier].primary = override.primary;
      if (override.fallback) result[tier].fallback = override.fallback;
    }
  }
  return result;
}
function applyUnavailableModels(tierConfigs, unavailableModels) {
  if (!unavailableModels || unavailableModels.length === 0) return tierConfigs;
  const dead = new Set(unavailableModels);
  let result = tierConfigs;
  for (const tier of Object.keys(tierConfigs)) {
    const config = tierConfigs[tier];
    const alive = [config.primary, ...config.fallback].filter((model) => !dead.has(model));
    if (alive.length === 0 || alive[0] === config.primary && alive.length === config.fallback.length + 1) {
      continue;
    }
    if (result === tierConfigs) result = { ...tierConfigs };
    result[tier] = { primary: alive[0], fallback: alive.slice(1) };
  }
  return result;
}
var RulesStrategy = class {
  name = "rules";
  route(prompt, systemPrompt, maxOutputTokens, options) {
    const { config, modelPricing } = options;
    const fullText = `${systemPrompt ?? ""} ${prompt}`;
    const estimatedTokens = estimateTokens(fullText);
    const limit = scanLimit(config.classifier.promptTruncationChars);
    const scannedPrompt = sampleForScan(prompt, limit);
    const scannedSystemPrompt = systemPrompt ? sampleForScan(systemPrompt, limit) : void 0;
    const ruleResult = classifyByRules(
      scannedPrompt,
      scannedSystemPrompt,
      estimatedTokens,
      config.scoring
    );
    const { routingProfile } = options;
    let tierConfigs;
    let profileSuffix;
    let profile;
    if (routingProfile === "eco") {
      tierConfigs = config.ecoTiers ?? config.tiers;
      profileSuffix = config.ecoTiers ? " | eco" : " | eco (default tiers)";
      profile = "eco";
    } else if (routingProfile === "premium") {
      tierConfigs = config.premiumTiers ?? config.tiers;
      profileSuffix = config.premiumTiers ? " | premium" : " | premium (default tiers)";
      profile = "premium";
    } else {
      const agenticScore = ruleResult.agenticScore ?? 0;
      const isAutoAgentic = agenticScore >= 0.5;
      const agenticModeSetting = config.overrides.agenticMode;
      const hasToolsInRequest = options.requiresTools ?? options.hasTools ?? false;
      let useAgenticTiers;
      if (agenticModeSetting === false) {
        useAgenticTiers = false;
      } else if (agenticModeSetting === true) {
        useAgenticTiers = config.agenticTiers != null;
      } else {
        useAgenticTiers = (hasToolsInRequest || isAutoAgentic) && config.agenticTiers != null;
      }
      tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
      profileSuffix = useAgenticTiers ? ` | agentic${hasToolsInRequest ? " (tools)" : ""}` : "";
      profile = useAgenticTiers ? "agentic" : "auto";
    }
    tierConfigs = applyPromotions(tierConfigs, config.promotions, profile, options.now);
    tierConfigs = applyUnavailableModels(tierConfigs, options.unavailableModels);
    const agenticScoreValue = ruleResult.agenticScore;
    if (estimatedTokens > config.overrides.maxTokensForceComplex) {
      const decision2 = selectModel(
        "COMPLEX",
        0.95,
        "rules",
        `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${profileSuffix}`,
        tierConfigs,
        modelPricing,
        estimatedTokens,
        maxOutputTokens,
        routingProfile,
        agenticScoreValue
      );
      return { ...decision2, tierConfigs, profile };
    }
    const hasStructuredOutput = options.requiresStructuredOutput === true;
    let tier;
    let confidence;
    const method = "rules";
    let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
    if (ruleResult.tier !== null) {
      tier = ruleResult.tier;
      confidence = ruleResult.confidence;
    } else {
      tier = config.overrides.ambiguousDefaultTier;
      confidence = 0.5;
      reasoning += ` | ambiguous -> default: ${tier}`;
    }
    if (hasStructuredOutput) {
      const tierRank = { SIMPLE: 0, MEDIUM: 1, COMPLEX: 2, REASONING: 3 };
      const minTier = config.overrides.structuredOutputMinTier;
      if (tierRank[tier] < tierRank[minTier]) {
        reasoning += ` | upgraded to ${minTier} (structured output)`;
        tier = minTier;
      }
    }
    reasoning += profileSuffix;
    const decision = selectModel(
      tier,
      confidence,
      method,
      reasoning,
      tierConfigs,
      modelPricing,
      estimatedTokens,
      maxOutputTokens,
      routingProfile,
      agenticScoreValue
    );
    return { ...decision, tierConfigs, profile };
  }
};
var registry = /* @__PURE__ */ new Map();
registry.set("rules", new RulesStrategy());
function getStrategy(name) {
  const strategy = registry.get(name);
  if (!strategy) {
    throw new Error(`Unknown routing strategy: ${name}`);
  }
  return strategy;
}
function registerStrategy(strategy) {
  registry.set(strategy.name, strategy);
}
var DEFAULT_MODEL_CAPABILITIES = Object.freeze({
  "anthropic/claude-fable-5": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-fable-5.1": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-haiku-4.5": {
    // override: The public catalog's `categories` omit "vision" for this Anthropic model even though the gateway accepts image input for it (the prior hand-maintained snapshot had it, and Anthropic's model card lists it). Without this the vision filter would silently drop it — reported against the catalog; remove once the categories carry vision.
    contextWindow: 2e5,
    maxOutputTokens: 64e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-opus-4.5": {
    contextWindow: 2e5,
    maxOutputTokens: 64e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-opus-4.7": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-opus-4.8": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-opus-5": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-opus-5.5": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-sonnet-4.5": {
    contextWindow: 2e5,
    maxOutputTokens: 64e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-sonnet-4.6": {
    // override: The public catalog's `categories` omit "vision" for this Anthropic model even though the gateway accepts image input for it (the prior hand-maintained snapshot had it, and Anthropic's model card lists it). Without this the vision filter would silently drop it — reported against the catalog; remove once the categories carry vision.
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-sonnet-5": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "anthropic/claude-sonnet-5.5": {
    contextWindow: 1e6,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "cohere/north-mini-code": {
    // supportsTools: not probed — fails closed
    contextWindow: 256e3,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "deepseek/deepseek-chat": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "deepseek/deepseek-reasoner": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "deepseek/deepseek-v4-flash-vision-exp": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: false,
    supportsVision: true
  },
  "deepseek/deepseek-v4-pro": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "deepseek/deepseek-v4.1-flash": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "google/gemini-2.5-flash": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-2.5-flash-lite": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "google/gemini-2.5-pro": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-3-flash-preview": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-3.1-flash-lite": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "google/gemini-3.1-pro": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-3.5-flash": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-3.5-flash-lite": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "google/gemini-3.6-flash": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-3.8-flash": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "google/gemini-4-argon": {
    // supportsTools: not probed — fails closed
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: false,
    supportsVision: true
  },
  "minimax/minimax-m2.7": {
    contextWindow: 204800,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: false
  },
  "minimax/minimax-m3": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "mistral/mistral-large-4": {
    // supportsTools: not probed — fails closed
    contextWindow: 524288,
    maxOutputTokens: 262144,
    supportsTools: false,
    supportsVision: true
  },
  "moonshot/kimi-k3": {
    contextWindow: 1048576,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: true
  },
  "nvidia/llama-3.2-11b-vision": {
    // supportsTools: not probed — fails closed
    contextWindow: 128e3,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: true
  },
  "nvidia/muse-glimmer-30b": {
    // supportsTools: not probed — fails closed
    contextWindow: 131072,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {
    // override: The catalog tags this model "vision", but a correctly sized probe does not hold up (ClawRouter, 2026-08-31): a 64x64 solid-red PNG was named correctly 1 of 4 times on Base, and on Solana the image was silently dropped and a text model answered "white". An HTTP 200 with a confident wrong answer gives the caller nothing to branch on, so image turns must not be routed here. It is ecoTiers.SIMPLE.fallback[0] since nemotron-3-nano-30b was delisted (2026-09-08); remove once a probe of this size comes back right on both chains.
    contextWindow: 256e3,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "nvidia/nemotron-3-ultra-550b": {
    // supportsTools: not probed — fails closed
    contextWindow: 1e6,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "nvidia/nemotron-3.5-lightning": {
    // supportsTools: not probed — fails closed
    contextWindow: 1e6,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "openai/chat-latest": {
    contextWindow: 128e3,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-4.1": {
    contextWindow: 128e3,
    maxOutputTokens: 32768,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-4.1-mini": {
    contextWindow: 128e3,
    maxOutputTokens: 32768,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-4.1-nano": {
    contextWindow: 128e3,
    maxOutputTokens: 32768,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-4o": {
    contextWindow: 128e3,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-4o-mini": {
    contextWindow: 128e3,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-5-mini": {
    contextWindow: 2e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-5.1": {
    contextWindow: 4e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.2": {
    contextWindow: 4e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.2-pro": {
    // supportsTools: not probed — fails closed
    contextWindow: 4e5,
    maxOutputTokens: 128e3,
    supportsTools: false,
    supportsVision: true
  },
  "openai/gpt-5.3-codex": {
    // supportsTools: gateway unavailable at probe time — fails closed; override: 2026-08-29 probe: every request (6 plain + 3 tool attempts) returned a gateway 500, so the probe measured an incident, not the model. Codex's function calling is established by the 2026-07 Terminal-Bench / tau2 calibration trajectories in portfolio.ts. Hosts observing the 500s should drop it with RouterOptions.unavailableModels rather than this snapshot claiming the model cannot call tools.
    contextWindow: 4e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-5.4": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.4-mini": {
    contextWindow: 4e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.4-nano": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "openai/gpt-5.4-pro": {
    // supportsTools: not probed — fails closed
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: false,
    supportsVision: true
  },
  "openai/gpt-5.5": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.5-pro": {
    // supportsTools: not probed — fails closed
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: false,
    supportsVision: true
  },
  "openai/gpt-5.6-luna": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.6-luna-pro": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: false,
    supportsVision: true
  },
  "openai/gpt-5.6-sol": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.6-sol-pro": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.6-terra": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-5.6-terra-pro": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-6-astra": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-6-luna": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-6-sol": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/gpt-6.1-sol": {
    contextWindow: 105e4,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: true
  },
  "openai/o1": {
    contextWindow: 2e5,
    maxOutputTokens: 1e5,
    supportsTools: true,
    supportsVision: false
  },
  "openai/o3": {
    contextWindow: 2e5,
    maxOutputTokens: 1e5,
    supportsTools: true,
    supportsVision: false
  },
  "openai/o3-mini": {
    contextWindow: 128e3,
    maxOutputTokens: 1e5,
    supportsTools: true,
    supportsVision: false
  },
  "openai/o4-mini": {
    contextWindow: 128e3,
    maxOutputTokens: 1e5,
    supportsTools: true,
    supportsVision: false
  },
  "poolside/laguna-s-2.1": {
    // supportsTools: not probed — fails closed
    contextWindow: 1048576,
    maxOutputTokens: 131072,
    supportsTools: false,
    supportsVision: false
  },
  "poolside/laguna-xs-2.1": {
    // supportsTools: not probed — fails closed
    contextWindow: 131072,
    maxOutputTokens: 16384,
    supportsTools: false,
    supportsVision: false
  },
  "qwen/qwen3.7-flash": {
    contextWindow: 1e6,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "qwen/qwen3.7-max": {
    contextWindow: 1e6,
    maxOutputTokens: 65536,
    supportsTools: true,
    supportsVision: false
  },
  "qwen/qwen3.7-plus": {
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: false
  },
  "qwen/qwen3.8-flash": {
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: false,
    supportsVision: true
  },
  "qwen/qwen3.8-max": {
    // supportsTools: not probed — fails closed
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: false,
    supportsVision: false
  },
  "tencent/hy4-preview": {
    // supportsTools: not probed — fails closed
    contextWindow: 1048576,
    maxOutputTokens: 64e3,
    supportsTools: false,
    supportsVision: false
  },
  "xai/grok-4.3": {
    contextWindow: 1e6,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: true
  },
  "xai/grok-4.5": {
    contextWindow: 5e5,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: true
  },
  "xai/grok-4.6": {
    contextWindow: 5e5,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: true
  },
  "xai/grok-4.7": {
    contextWindow: 5e5,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: true
  },
  "xai/grok-build-0.1": {
    contextWindow: 256e3,
    maxOutputTokens: 16384,
    supportsTools: true,
    supportsVision: false
  },
  "xiaomi/mimo-v2.5": {
    contextWindow: 1048576,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: true
  },
  "xiaomi/mimo-v2.5-pro": {
    contextWindow: 1048576,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5": {
    contextWindow: 2e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5-turbo": {
    contextWindow: 2e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5.1": {
    contextWindow: 2e5,
    maxOutputTokens: 128e3,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5.2": {
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5.3": {
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: false
  },
  "zai/glm-5.3-flash": {
    contextWindow: 1e6,
    maxOutputTokens: 131072,
    supportsTools: true,
    supportsVision: true
  }
});
var model_profiles_generated_default = {
  "anthropic/claude-fable-5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 9298.5,
    p95LatencyMs: 9873.4,
    outputTokensPerSecond: 55.17,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-haiku-4.5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3157.4,
    p95LatencyMs: 3170.7,
    outputTokensPerSecond: 162.16,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-opus-4.5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6497.7,
    p95LatencyMs: 6953.7,
    outputTokensPerSecond: 78.99,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-opus-4.7": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5316.5,
    p95LatencyMs: 6121.5,
    outputTokensPerSecond: 97.34,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-opus-4.8": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6216.1,
    p95LatencyMs: 6847.7,
    outputTokensPerSecond: 82.81,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-opus-5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 7309,
    p95LatencyMs: 7745.2,
    outputTokensPerSecond: 70.17,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-sonnet-4.5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6330.4,
    p95LatencyMs: 6631.6,
    outputTokensPerSecond: 81.03,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-sonnet-4.6": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6508,
    p95LatencyMs: 6698.3,
    outputTokensPerSecond: 78.6,
    errorRate: 0,
    samples: 3
  },
  "anthropic/claude-sonnet-5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6165.4,
    p95LatencyMs: 6582.9,
    outputTokensPerSecond: 83.62,
    errorRate: 0,
    samples: 3
  },
  "deepseek/deepseek-chat": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4351.4,
    p95LatencyMs: 4543.7,
    outputTokensPerSecond: 117.78,
    errorRate: 0,
    samples: 3
  },
  "deepseek/deepseek-reasoner": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5201.2,
    p95LatencyMs: 6079.6,
    outputTokensPerSecond: 99.77,
    errorRate: 0,
    samples: 3
  },
  "deepseek/deepseek-v4-pro": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 8781.2,
    p95LatencyMs: 9881.1,
    outputTokensPerSecond: 58.98,
    errorRate: 0,
    samples: 3
  },
  "deepseek/deepseek-v4.1-flash": {
    measuredAt: "2026-10-20T18:12:51Z",
    latencyMs: 2383.5,
    p95LatencyMs: 2547.8,
    outputTokensPerSecond: 216.53,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-2.5-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5416.4,
    p95LatencyMs: 6442.8,
    outputTokensPerSecond: 213.07,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-2.5-flash-lite": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5002.6,
    p95LatencyMs: 5780.3,
    outputTokensPerSecond: 408.43,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-2.5-pro": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 28169.5,
    p95LatencyMs: 29491.4,
    outputTokensPerSecond: 147.3,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3-flash-preview": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4717.1,
    p95LatencyMs: 5037.1,
    outputTokensPerSecond: 198.71,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3.1-flash-lite": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 2855.8,
    p95LatencyMs: 3172.7,
    outputTokensPerSecond: 286.91,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3.1-pro": {
    measuredAt: "2026-08-29T16:59:54Z",
    latencyMs: 24194.1,
    p95LatencyMs: 27269.6,
    outputTokensPerSecond: 109.47,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3.5-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5320.6,
    p95LatencyMs: 5429.8,
    outputTokensPerSecond: 226.21,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3.5-flash-lite": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3515.8,
    p95LatencyMs: 4363.4,
    outputTokensPerSecond: 248.9,
    errorRate: 0,
    samples: 3
  },
  "google/gemini-3.6-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 13020,
    p95LatencyMs: 15383.1,
    outputTokensPerSecond: 187.87,
    errorRate: 0,
    samples: 3
  },
  "minimax/minimax-m2.7": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 8761.1,
    p95LatencyMs: 10199.3,
    outputTokensPerSecond: 59.18,
    errorRate: 0,
    samples: 3
  },
  "minimax/minimax-m3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 11101.9,
    p95LatencyMs: 26087.1,
    outputTokensPerSecond: 101.12,
    errorRate: 0,
    samples: 3
  },
  "moonshot/kimi-k3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 24498.9,
    p95LatencyMs: 40365.3,
    outputTokensPerSecond: 25.11,
    errorRate: 0,
    samples: 3
  },
  "nvidia/mistral-nemotron": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 7349.6,
    p95LatencyMs: 9932.3,
    outputTokensPerSecond: 79.48,
    errorRate: 0.3333,
    samples: 3
  },
  "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {
    measuredAt: "2026-08-29T16:59:54Z",
    latencyMs: 9324.6,
    p95LatencyMs: 12992,
    outputTokensPerSecond: 64.96,
    errorRate: 0.3333,
    samples: 3
  },
  "nvidia/nemotron-nano-12b-v2-vl": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5846.9,
    p95LatencyMs: 5846.9,
    outputTokensPerSecond: 87.57,
    errorRate: 0.6667,
    samples: 3
  },
  "nvidia/nemotron-nano-9b-v2": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5282.5,
    p95LatencyMs: 5282.5,
    outputTokensPerSecond: 96.92,
    errorRate: 0.6667,
    samples: 3
  },
  "nvidia/step-3.7-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4617.4,
    p95LatencyMs: 5237.4,
    outputTokensPerSecond: 112.92,
    errorRate: 0.3333,
    samples: 3
  },
  "openai/chat-latest": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3690.9,
    p95LatencyMs: 4344,
    outputTokensPerSecond: 111.85,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-4.1": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3527.9,
    p95LatencyMs: 3831.7,
    outputTokensPerSecond: 141.27,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-4.1-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4268.2,
    p95LatencyMs: 5101.5,
    outputTokensPerSecond: 103.42,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-4.1-nano": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3088.3,
    p95LatencyMs: 3369.2,
    outputTokensPerSecond: 150.31,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-4o": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 2995.2,
    p95LatencyMs: 3174.2,
    outputTokensPerSecond: 171.32,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-4o-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4751.5,
    p95LatencyMs: 4930.4,
    outputTokensPerSecond: 107.84,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4558.1,
    p95LatencyMs: 5081.9,
    outputTokensPerSecond: 113.25,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.2": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5436.6,
    p95LatencyMs: 5928.8,
    outputTokensPerSecond: 95.47,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.3-codex": {
    measuredAt: "2026-08-29T16:59:54Z",
    latencyMs: 15290.4,
    p95LatencyMs: 15290.4,
    outputTokensPerSecond: 33.49,
    errorRate: 0.6667,
    samples: 3
  },
  "openai/gpt-5.4": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5596,
    p95LatencyMs: 5919.4,
    outputTokensPerSecond: 91.67,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.4-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3377.8,
    p95LatencyMs: 3646.8,
    outputTokensPerSecond: 138.08,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.4-nano": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4040.4,
    p95LatencyMs: 4205.9,
    outputTokensPerSecond: 118.52,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6367.8,
    p95LatencyMs: 7330.7,
    outputTokensPerSecond: 81.29,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.6-luna": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6064.5,
    p95LatencyMs: 7347.3,
    outputTokensPerSecond: 87.93,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.6-luna-pro": {
    measuredAt: "2026-08-29T16:59:54Z",
    latencyMs: 13914.9,
    p95LatencyMs: 13914.9,
    outputTokensPerSecond: 36.79,
    errorRate: 0.6667,
    samples: 3
  },
  "openai/gpt-5.6-sol": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 7720.2,
    p95LatencyMs: 9108.2,
    outputTokensPerSecond: 67.47,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.6-sol-pro": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 11442.7,
    p95LatencyMs: 13363.1,
    outputTokensPerSecond: 148.75,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.6-terra": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4941,
    p95LatencyMs: 5095.3,
    outputTokensPerSecond: 103.69,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-5.6-terra-pro": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3574.1,
    p95LatencyMs: 4126.3,
    outputTokensPerSecond: 133.59,
    errorRate: 0,
    samples: 3
  },
  "openai/gpt-6.1-sol": {
    measuredAt: "2026-10-20T18:12:51Z",
    latencyMs: 11324,
    p95LatencyMs: 12747.6,
    outputTokensPerSecond: 45.11,
    errorRate: 0,
    samples: 3
  },
  "openai/o1": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4324.9,
    p95LatencyMs: 5838.1,
    outputTokensPerSecond: 125.86,
    errorRate: 0,
    samples: 3
  },
  "openai/o3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 5463.4,
    p95LatencyMs: 5613.1,
    outputTokensPerSecond: 93.8,
    errorRate: 0,
    samples: 3
  },
  "openai/o3-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 2912.7,
    p95LatencyMs: 3092.1,
    outputTokensPerSecond: 176.49,
    errorRate: 0,
    samples: 3
  },
  "openai/o4-mini": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 4958.7,
    p95LatencyMs: 5313,
    outputTokensPerSecond: 103.81,
    errorRate: 0,
    samples: 3
  },
  "qwen/qwen3.7-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 3385.5,
    p95LatencyMs: 4042.7,
    outputTokensPerSecond: 153.94,
    errorRate: 0,
    samples: 3
  },
  "qwen/qwen3.7-max": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 9387.1,
    p95LatencyMs: 10490.2,
    outputTokensPerSecond: 54.92,
    errorRate: 0,
    samples: 3
  },
  "qwen/qwen3.7-plus": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 9766.6,
    p95LatencyMs: 9798.2,
    outputTokensPerSecond: 52.42,
    errorRate: 0,
    samples: 3
  },
  "tencent/hy3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6062.3,
    p95LatencyMs: 7070.2,
    outputTokensPerSecond: 87.3,
    errorRate: 0,
    samples: 3
  },
  "xai/grok-4.3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 9467.7,
    p95LatencyMs: 10087.9,
    outputTokensPerSecond: 48.36,
    errorRate: 0,
    samples: 3
  },
  "xai/grok-4.5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 13564.8,
    p95LatencyMs: 17351.9,
    outputTokensPerSecond: 60.71,
    errorRate: 0,
    samples: 3
  },
  "xai/grok-build-0.1": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 16394.8,
    p95LatencyMs: 18035.4,
    outputTokensPerSecond: 96.86,
    errorRate: 0,
    samples: 3
  },
  "xiaomi/mimo-v2.5-pro": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 12070.7,
    p95LatencyMs: 12386.8,
    outputTokensPerSecond: 42.44,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 6839.7,
    p95LatencyMs: 7261.4,
    outputTokensPerSecond: 75.16,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5-turbo": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 55348.5,
    p95LatencyMs: 114086.6,
    outputTokensPerSecond: 14.64,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5.1": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 15658.4,
    p95LatencyMs: 17307.1,
    outputTokensPerSecond: 32.9,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5.2": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 10308.5,
    p95LatencyMs: 15127.6,
    outputTokensPerSecond: 54.87,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5.3": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 7272.4,
    p95LatencyMs: 7998.1,
    outputTokensPerSecond: 71.09,
    errorRate: 0,
    samples: 3
  },
  "zai/glm-5.3-flash": {
    measuredAt: "2026-08-29T16:51:33Z",
    latencyMs: 10545.3,
    p95LatencyMs: 11672.4,
    outputTokensPerSecond: 49.01,
    errorRate: 0,
    samples: 3
  }
};
var LIVE_MODEL_PROFILES = Object.freeze(
  model_profiles_generated_default
);
var HISTORICAL_MODEL_PROFILES = Object.freeze({
  "anthropic/claude-haiku-4.5": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 2305,
    outputTokensPerSecond: 140.6
  },
  "anthropic/claude-sonnet-4.6": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 2110,
    outputTokensPerSecond: 121.3
  },
  "deepseek/deepseek-chat": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 1431,
    outputTokensPerSecond: 179.2,
    intelligenceIndex: 32
  },
  "google/gemini-2.5-flash": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 1238,
    outputTokensPerSecond: 207.6,
    intelligenceIndex: 20
  },
  "google/gemini-2.5-flash-lite": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 1353,
    outputTokensPerSecond: 192.5,
    intelligenceIndex: 20
  },
  "google/gemini-2.5-pro": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 1294,
    outputTokensPerSecond: 197.8
  },
  "google/gemini-3.1-pro": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 1609,
    outputTokensPerSecond: 167.2
  },
  "openai/gpt-4o-mini": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 2764,
    outputTokensPerSecond: 92.8
  },
  "openai/gpt-5.3-codex": {
    measuredAt: "2026-03-16T13:50:48Z",
    latencyMs: 7935,
    outputTokensPerSecond: 32.3
  }
});
function inferToolRequirement(prompt, _systemPrompt, toolChoice) {
  if (toolChoice === "none") return false;
  if (toolChoice === "required") return true;
  if (typeof toolChoice === "object" && toolChoice !== null && toolChoice.type === "function") {
    return true;
  }
  const text = prompt;
  const explicitTool = /\b(?:use|call|invoke)\s+(?:the\s+)?[\w.-]+\s+(?:tool|function|api)\b|\btool[_ -]?call\b|使用.{0,20}(?:工具|函数|接口)|调用.{0,20}(?:工具|函数|接口)/i;
  const codeEnvironment = /\b(?:run|execute)\s+(?:the\s+)?(?:tests?|command|script|build|linter)|\b(?:edit|modify|patch|create|write|save|delete|rename|move|inspect|read)\b.{0,60}\b(?:file|repository|repo|codebase|directory|folder)\b|\b(?:terminal|shell|bash|zsh|pytest|npm test|pnpm test|git\s+(?:status|diff|commit)|docker)\b|(?:运行|执行).{0,20}(?:测试|命令|脚本|构建)|(?:修改|编辑|修复|创建|读取|检查|保存).{0,30}(?:文件|仓库|代码库|目录)/i;
  const webAction = /\b(?:browse|search|look up|fetch|open)\b.{0,80}\b(?:web|website|url|online|documentation|docs|news|weather|price)\b|(?:浏览|搜索|查询|打开).{0,30}(?:网页|网站|链接|文档|新闻|天气|价格)/i;
  const statefulAction = /\b(?:refund|cancel|book|reserve|purchase|buy|return|exchange|transfer|update|change)\b.{0,80}\b(?:order|booking|reservation|account|address|payment|subscription|ticket|flight|item)\b|(?:退款|取消|预订|购买|退货|换货|转账|更新|修改).{0,30}(?:订单|预订|账户|地址|付款|订阅|票|航班|商品)/i;
  return explicitTool.test(text) || codeEnvironment.test(text) || webAction.test(text) || statefulAction.test(text);
}
var DEFAULT_PORTFOLIO_WEIGHTS = {
  auto: {
    quality: 0.47,
    capability: 0.2,
    cost: 0.18,
    speed: 0.07,
    reliability: 0.03,
    legacy: 0.05
  },
  eco: { quality: 0.36, capability: 0.2, cost: 0.28, speed: 0.1, reliability: 0.04, legacy: 0.02 },
  premium: {
    quality: 0.58,
    capability: 0.2,
    cost: 0.08,
    speed: 0.06,
    reliability: 0.06,
    legacy: 0.02
  },
  highStakesBoost: { quality: 0.08, reliability: 0.05 },
  latencySensitiveSpeedBoost: 0.08,
  affinityFloorGap: { auto: 0.1, eco: 0.22, premium: 0.05 }
};
function likelyNeedsParallelToolCalls(prompt, needsTools, toolCount, toolNames) {
  if (!needsTools || toolCount === void 0 || toolCount < 1) return false;
  const text = prompt.trim();
  const explicitRepeat = /\b(?:in parallel|simultaneously|concurrently|for each|each of|every one|both|(?:two|three|multiple|several)\s+(?:cities|locations|items|tasks|orders|users|files))\b|并行|同时|分别|每个|各自|(?:两个|三个|多个)(?:城市|地点|项目|任务|订单|用户|文件)|cada uno|para cada|simult[aá]neamente/i.test(
    text
  );
  if (explicitRepeat) return true;
  const sentenceClauses = text.split(/[.!?。!?]+/).map((part) => part.trim()).filter((part) => part.length >= 8);
  if (/\b(?:also|additionally|furthermore)\b|另外|此外|그리고/i.test(text) && sentenceClauses.length >= 2 || /\band\s+(?:also|for the)\b/i.test(text))
    return true;
  const pairedQuantity = /\b\d+(?:\.\d+)?\s+(?:and|or)\s+\d+(?:\.\d+)?\s*(?:gb|mb|tb|kg|g|ml|oz|cups?|cores?|cpus?)\b/i.test(
    text
  );
  if (pairedQuantity) return true;
  const operationTokens = /* @__PURE__ */ new Set([
    "add",
    "delete",
    "remove",
    "cancel",
    "return",
    "exchange",
    "modify",
    "book",
    "transfer",
    "send",
    "upload",
    "download",
    "create",
    "close"
  ]);
  const lowered = text.toLowerCase();
  const matchedOperationTokens = new Set(
    (toolNames ?? []).flatMap((name) => name.toLowerCase().split(/[^a-z0-9\u3400-\u9fff]+/)).filter((token) => operationTokens.has(token) && lowered.includes(token))
  );
  if (matchedOperationTokens.size >= 2) return true;
  const nonEmptyLines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
  const quantityMentions = text.match(
    /\b(?:\d+(?:\.\d+)?|one|two|three|four|five|six|seven|eight|nine|ten)\s*(?:oz|ounce|ounces|g|gram|grams|kg|ml|cups?|pieces?|tablespoons?)\b/gi
  ) ?? [];
  if (nonEmptyLines.length >= 2 && quantityMentions.length >= 2) return true;
  const repeatedLookup = /\b(?:weather|climate|clima|tiempo|temperature|snow|news|report)\b|天气|气象|温度|降雪|新闻|报告/i.test(
    text
  );
  const multiLocationConnector = /\b(?:and also|both|y|e)\b|还有|以及|和|、/i.test(text);
  const commaSeparatedLocations = (text.match(/[,,]/g) ?? []).length >= 2;
  if (repeatedLookup && (multiLocationConnector || commaSeparatedLocations)) return true;
  const distinctOrderParts = /\b(?:food|meal)\b[\s\S]*\bdrink\b|\bdrink\b[\s\S]*\b(?:food|meal)\b/i.test(text);
  const koreanParallelClauses = (text.match(/,/g) ?? []).length >= 3 && /하고|그리고/.test(text);
  return distinctOrderParts || koreanParallelClauses;
}
function classifyTask(prompt, systemPrompt, options) {
  const fullText = `${systemPrompt ?? ""} ${prompt}`;
  const estimatedInputTokens = estimateTokens(fullText);
  const limit = scanLimit(options.config.classifier.promptTruncationChars);
  const scannedPrompt = sampleForScan(prompt, limit);
  const scannedSystemPrompt = sampleForScan(systemPrompt ?? "", limit);
  const scannedFullText = `${scannedSystemPrompt} ${scannedPrompt}`;
  const text = scannedPrompt.toLowerCase();
  const explicitCodeSignal = /```|\b(?:typescript|javascript|python|rust|java|sql|stack trace|traceback|exception)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(
    scannedPrompt
  );
  const codeConstructSignal = /\b(?:implement|refactor|debug|write|edit|modify|create|define|review|fix)\b[\s\S]{0,48}\b(?:api|function|class|method)\b|\b(?:api|function|class|method)\b[\s\S]{0,48}\b(?:code|implementation|typescript|javascript|python|rust|java)\b/i.test(
    scannedPrompt
  );
  const nativeCodeSignal = /\b(?:programmed|written|implemented?|code)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b/i.test(
    scannedPrompt
  );
  const errorClassSignal = /\b[A-Z][A-Za-z]*(?:Error|Exception)\b/.test(scannedPrompt);
  const hasCode = explicitCodeSignal || codeConstructSignal || nativeCodeSignal || errorClassSignal;
  const codeArtifact = /```|\b(?:stack trace|traceback)\b|\.(?:ts|tsx|js|py|go|rs)\b/i.test(scannedPrompt) || codeConstructSignal || nativeCodeSignal || errorClassSignal;
  const toolsAvailable = options.hasTools ?? false;
  const needsTools = options.requiresTools ?? (toolsAvailable && inferToolRequirement(scannedPrompt, scannedSystemPrompt));
  const likelyParallelToolCalls = likelyNeedsParallelToolCalls(
    scannedPrompt,
    needsTools,
    options.toolCount,
    options.toolNames
  );
  const normalizedToolNames = (options.toolNames ?? []).map((name) => name.toLowerCase());
  const airlineToolSignal = normalizedToolNames.some(
    (name) => /(?:flight|reservation|airport|baggage|passenger)/.test(name)
  );
  const retailToolSignal = normalizedToolNames.some(
    (name) => /(?:order|product|item|return|exchange|address)/.test(name)
  );
  const webResearchToolSignal = normalizedToolNames.some(
    (name) => /^(?:web_?search|web_?fetch)$/.test(name)
  );
  const agentDomain = airlineToolSignal && !retailToolSignal ? "airline" : retailToolSignal && !airlineToolSignal ? "retail" : webResearchToolSignal ? "web_research" : "other";
  const clueConnectors = scannedFullText.match(
    /\b(?:after|before|while|where|whose|which|in \d{4}|as of|over \d+|another|also|furthermore)\b|(?:之后|之前|其中|截至|超过|另一个|此外)/gi
  ) ?? [];
  const entityResolutionSignal = /\b(?:identify|who (?:is|was)|what (?:is|was) the name|which (?:person|player|company|country|city)|find the (?:person|player|name|entity))\b|(?:找出|识别|是谁|哪位|名称是什么)/i.test(
    scannedFullText
  );
  const exactAnswerSignal = /\b(?:exact answer|single best-supported answer|following clues|multiple public sources)\b|(?:精确答案|根据.*线索|多个公开来源)/i.test(
    scannedFullText
  );
  const deepWebResearch = agentDomain === "web_research" && (exactAnswerSignal || entityResolutionSignal && (clueConnectors.length >= 3 || prompt.length >= 320));
  const globalOptimizationSignal = /\b(?:cheapest|lowest[- ]price|least expensive|most expensive|highest(?:[- ]priced)?|largest|smallest|maximum|minimum|best available|closest|not (?:cost|exceed))\b|最便宜|最低价|最贵|最高价|最大|最小/i.test(
    scannedPrompt
  );
  const globalScopeSignal = /\b(?:everything|all (?:(?:my|your|their|the) )?(?:future |upcoming )?(?:items|orders|passengers|flights|reservations|bookings)|every (?:item|order|passenger|flight|reservation|booking))\b|全部|所有|每个/i.test(
    scannedPrompt
  );
  const globalChoiceSignal = globalOptimizationSignal || globalScopeSignal;
  const crossRecordSignal = /\b(?:another|other|different|previous)\s+(?:order|reservation|booking|account|address)\b|另一(?:个)?(?:订单|预订|账户|地址)|其他(?:订单|预订|账户|地址)/i.test(
    scannedPrompt
  );
  const reservationIds = scannedPrompt.match(/\b[A-Z0-9]{6}\b/g) ?? [];
  const crossReservationBatchSignal = agentDomain === "airline" && (/\b(?:two|three|multiple|several)(?:\s+of\s+(?:my|our|the))?\s+(?:upcoming\s+)?(?:reservations?|bookings?)\b|\b(?:a\s+)?(?:second|third)\s+(?:reservation|booking)\b/i.test(
    scannedPrompt
  ) || new Set(reservationIds).size >= 2);
  const conditionalGlobalWorkflowSignal = agentDomain === "airline" && globalScopeSignal && /\b(?:if|that (?:contain|have)|longer than|shorter than|under|over|at (?:most|least)|wherever possible)\b|如果|超过|少于|不超过|尽可能/i.test(
    scannedPrompt
  ) && /\b(?:cancel|change|upgrade|move|book)\b[\s\S]*\b(?:cancel|change|upgrade|move|book)\b|取消[\s\S]*(?:升级|更改)|升级[\s\S]*(?:取消|更改)/i.test(
    scannedPrompt
  );
  const policyExceptionSignal = agentDomain === "retail" && /\b(?:return|refund|send back|get (?:my |the )?money back)\b|退货|退款|退回/i.test(
    scannedPrompt
  ) && /\b(?:amex|american express|visa|mastercard|credit card|debit card|different card|another card|other card)\b|信用卡|借记卡|其他卡|另一张卡/i.test(
    scannedPrompt
  );
  const singleSelectedPolicyException = policyExceptionSignal && /\b(?:return|refund|send back)\b[^.!?。!?]{0,96}\b(?:the )?(?:pricier|cheaper|more expensive|less expensive|costlier|one)\b/i.test(
    scannedPrompt
  );
  const negotiatedWorkflowSignal = agentDomain === "retail" && /\b(?:return|exchange)\b|退货|退回|换货|交换/i.test(scannedPrompt);
  const numberedSteps = (scannedPrompt.match(/(?:^|\s)\d+(?:\.\d+)*[.)]\s+/g) ?? []).length;
  const complexMultiToolPlan = likelyParallelToolCalls && ((options.toolCount ?? 0) >= 6 || numberedSteps >= 3 || prompt.length > 1200);
  let agentRisk = needsTools && singleSelectedPolicyException ? "policy_exception_simple" : needsTools && policyExceptionSignal ? "policy_exception" : (
    // Airline prompts that require a global optimum (for example the
    // cheapest itinerary across several candidates) are materially harder
    // than applying one change to every passenger in a known reservation.
    // Full-session evidence supports Sonnet for the former, while upgrading
    // the latter merely because it says "all passengers" caused a large cost
    // increase without a quality gain.
    needsTools && agentDomain === "airline" && (globalOptimizationSignal || conditionalGlobalWorkflowSignal) ? "complex_high" : needsTools && (likelyParallelToolCalls || globalChoiceSignal || crossRecordSignal || crossReservationBatchSignal || negotiatedWorkflowSignal) ? "high" : "standard"
  );
  const needsVision = options.hasVision ?? false;
  const needsStructuredOutput = options.requiresStructuredOutput ?? false;
  const latencySensitive = /\b(?:urgent|asap|fast|quick|low latency|real[- ]time)\b|尽快|马上|快速|低延迟/i.test(
    scannedPrompt
  );
  const highStakes = /\b(?:production|security|payment|legal|medical|financial|audit)\b|生产|安全|支付|法律|医疗|财务|审计/i.test(
    scannedPrompt
  );
  const terminalToolSignal = normalizedToolNames.some(
    (name) => /^(?:terminalexec|terminalinspect|terminalsendkeys)$/.test(name)
  );
  const simpleTerminalArtifact = /\b(?:create|write|convert|generate|build|implement|run|fix|repair|debug|make)\b[\s\S]{0,120}\b(?:file|script|csv|parquet|json|txt|server|endpoint)\b/i.test(
    scannedPrompt
  );
  const terminalComplexRepair = terminalToolSignal && /\b(?:multiple|several)\s+(?:scripts?|files?|components?)\b|\b(?:pipeline|dependencies)\b[\s\S]{0,100}\b(?:fail|issue|fix|repair|run|execute)\b|\b(?:identify|find|fix|repair)\s+(?:and\s+)?(?:fix\s+)?all\s+(?:the\s+)?issues\b/i.test(
    scannedPrompt
  );
  const mentionedTerminalRuntimes = new Set(
    (scannedPrompt.match(/\b(?:gcc|clang|rustc|javac|go\s+build|node|python)\b/gi) ?? []).map(
      (name) => name.toLowerCase().replace(/\s+/g, " ")
    )
  );
  const terminalCrossRuntimeArtifact = terminalToolSignal && (/\bpolyglot\b/i.test(scannedPrompt) || /\b(?:both|each)\b[\s\S]{0,120}\b(?:compilers?|runtimes?|toolchains?)\b/i.test(
    scannedPrompt
  ) || mentionedTerminalRuntimes.size >= 2 && /\b(?:compile|build|run|execute)\b/i.test(scannedPrompt));
  const terminalFrameworkToNativeArtifact = terminalToolSignal && /\b(?:pytorch|tensorflow|jax|onnx|state[_ -]?dict|checkpoint|safetensors?)\b|\.(?:pth|pt|onnx)\b/i.test(
    scannedPrompt
  ) && /\b(?:pure|native|programmed|written|implemented?)\s+(?:in|using)\s+(?:c\+\+|c|rust|go)\b|\b(?:c\+\+|c|rust|go)\s+(?:program|binary|executable|cli|tool|implementation)\b/i.test(
    scannedPrompt
  ) && /\b(?:inference|model|weights?|tensor|export|convert|load)\b/i.test(scannedPrompt);
  if (needsTools && (terminalComplexRepair || terminalCrossRuntimeArtifact || terminalFrameworkToNativeArtifact) && (agentRisk === "standard" || agentRisk === "high"))
    agentRisk = "complex_high";
  const complexTerminalOperation = /\b(?:git|ssh|nginx|https|certificate|authentication|credential|deploy|production|encrypt|gpg|shred|securely delete|decommission|benchmark|evaluate|embedding|chess|image|search the web|schema|statistical|statistics|aggregate|join|multiple inputs?)\b/i.test(
    scannedPrompt
  );
  const terminalCredentialSignal = /\b(?:ssh|nginx|certificate|authentication|credentials?|passwords?|api keys?|deploy|production|encrypt|gpg|shred|securely delete|decommission)\b/i.test(
    scannedPrompt
  ) || /\b(?:access|auth|authentication|bearer|secret|api)\s+tokens?\b|\btokens?\s+(?:secret|credential|authentication)\b/i.test(
    scannedPrompt
  );
  const terminalSafetySensitive = terminalToolSignal && (highStakes || terminalCredentialSignal);
  const implicitTerminalCode = needsTools && terminalToolSignal && agentRisk === "standard" && !highStakes && !complexTerminalOperation && numberedSteps < 3 && prompt.length <= 1e3 && simpleTerminalArtifact;
  const language = /[\u3400-\u9fff]/.test(scannedPrompt) ? "zh" : "other";
  const multipleChoiceSignals = (scannedPrompt.match(/(?:^|\n)\s*[A-D][.)]\s+/gim) ?? []).length;
  const numericSignals = (scannedPrompt.match(/-?\d+(?:[.,]\d+)?/g) ?? []).length;
  const compactMathProblem = !hasCode && prompt.length < 2500 && numericSignals >= 2 && (/[+×÷=%$€£¥]|\b(?:total|each|per|times|half|twice|percent|how many|how much|calculate)\b/i.test(
    scannedPrompt
  ) || /[??]\s*$/.test(scannedPrompt.trim()) || numericSignals >= 3);
  let taskType = "chat";
  if (needsVision) taskType = "vision";
  else if (estimatedInputTokens > 8e4) taskType = "long_context";
  else if (needsTools && (hasCode || implicitTerminalCode)) taskType = "code_agent";
  else if (needsTools && likelyParallelToolCalls && !complexMultiToolPlan)
    taskType = "tool_agent_parallel";
  else if (needsTools) taskType = "tool_agent";
  else if (multipleChoiceSignals >= 3) taskType = "reasoning_mcq";
  else if (compactMathProblem) taskType = "reasoning_math";
  else if (errorClassSignal || /\b(?:bug|debug|error|failure|failing|regression|crash|修复|报错|错误|调试)\b/i.test(text))
    taskType = "debug";
  else if (hasCode || /\b(?:refactor|implement|patch|edit|rewrite|重构|实现|修改)\b/i.test(text))
    taskType = "code_edit";
  else if (needsStructuredOutput || /\b(?:extract|json|schema|csv|字段|提取)\b/i.test(text))
    taskType = "extraction";
  else if (/\b(?:prove|derive|theorem|formal|mathematical|reasoning|证明|推导|定理|数学)\b/i.test(text))
    taskType = "reasoning";
  return {
    taskType,
    estimatedInputTokens,
    hasCode,
    needsTools,
    toolsAvailable,
    needsVision,
    needsStructuredOutput,
    latencySensitive,
    highStakes,
    language,
    likelyParallelToolCalls,
    complexMultiToolPlan,
    agentDomain,
    deepWebResearch,
    agentRisk,
    terminalToolSignal,
    terminalSafetySensitive,
    implicitTerminalCode,
    codeArtifact
  };
}
function affinity(modelId, task, language = "other", agentDomain = "other", deepWebResearch = false, agentRisk = "standard", terminalToolSignal = false, terminalSafetySensitive = false) {
  const id = modelId.toLowerCase();
  const modelName = id.slice(id.indexOf("/") + 1);
  const match = (values, score) => values.some((value) => modelName === value) ? score : 0;
  const base = 0.68;
  switch (task) {
    case "code_agent":
      if (terminalToolSignal && agentRisk === "complex_high") {
        return Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5.3-codex"], 0.87),
          match(["gpt-5-mini"], 0.78),
          match(["gemini-3.5-flash"], 0.76)
        );
      }
      return Math.max(
        base,
        match(["gpt-5.3-codex"], 1),
        match(["claude-sonnet-5"], 0.98),
        match(["gpt-5-mini"], 0.96),
        match(["gemini-3.5-flash"], 0.92),
        match(["kimi-k3"], 0.9),
        match(["deepseek-v4-pro", "glm-5.2"], 0.88)
      );
    case "tool_agent":
      if (terminalToolSignal && agentRisk === "complex_high") {
        return Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5.3-codex"], 0.87),
          match(["gpt-5-mini"], 0.78),
          match(["gemini-3.5-flash"], 0.76)
        );
      }
      if (terminalToolSignal && !terminalSafetySensitive) {
        return Math.max(
          base,
          match(["gpt-5-mini"], 1),
          match(["gpt-5.3-codex"], 0.98),
          match(["claude-sonnet-5"], 0.9),
          match(["gemini-3.5-flash"], 0.89)
        );
      }
      if (terminalToolSignal && terminalSafetySensitive) {
        return Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["claude-opus-4.8"], 0.9),
          match(["gpt-5.3-codex"], 0.84)
        );
      }
      if (agentDomain === "web_research") {
        return deepWebResearch ? Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5-mini"], 0.88),
          match(["gemini-3.5-flash"], 0.84),
          match(["claude-opus-5"], 0.8),
          match(["claude-opus-4.8"], 0.78)
        ) : Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5-mini"], 0.88),
          match(["gemini-3.5-flash"], 0.86),
          match(["claude-opus-5"], 0.84),
          match(["claude-opus-4.8"], 0.82)
        );
      }
      if (agentDomain === "retail") {
        if (agentRisk === "standard") {
          return Math.max(
            base,
            match(["gpt-5-mini"], 1),
            match(["claude-sonnet-5"], 0.88),
            match(["gemini-3.5-flash"], 0.82),
            match(["gpt-5.3-codex"], 0.81),
            match(["kimi-k3"], 0.78),
            match(["deepseek-v4-pro"], 0.76)
          );
        }
        if (agentRisk === "policy_exception") {
          return Math.max(
            base,
            match(["gpt-4.1"], 1),
            match(["claude-sonnet-5"], 0.9),
            match(["deepseek-v4-pro"], 0.82),
            match(["gpt-5-mini"], 0.8),
            match(["gpt-4o-mini"], 0.76)
          );
        }
        if (agentRisk === "policy_exception_simple") {
          return Math.max(
            base,
            match(["gpt-5-mini"], 1),
            match(["gpt-4.1"], 0.86),
            match(["deepseek-v4-pro"], 0.82),
            match(["gpt-4o-mini"], 0.8)
          );
        }
        return Math.max(
          base,
          match(["deepseek-v4-pro"], 1),
          match(["claude-sonnet-5"], 0.88),
          match(["gemini-3.5-flash"], 0.82),
          match(["gpt-5.3-codex"], 0.81),
          match(["kimi-k3"], 0.78),
          match(["gpt-5-mini"], 0.76)
        );
      }
      if (agentDomain === "airline") {
        if (agentRisk === "complex_high") {
          return Math.max(
            base,
            match(["claude-sonnet-5"], 1),
            match(["gpt-5-mini"], 0.78),
            match(["gemini-3.5-flash"], 0.76),
            match(["deepseek-v4-pro"], 0.74)
          );
        }
        return Math.max(
          base,
          match(["gpt-5-mini"], 1),
          match(["claude-sonnet-5"], 0.9),
          match(["gemini-3.5-flash"], 0.8),
          match(["deepseek-v4-pro"], 0.76)
        );
      }
      return Math.max(
        base,
        match(["claude-sonnet-5"], 1),
        match(["gemini-3.5-flash"], 0.88),
        match(["gpt-5.3-codex"], 0.87),
        match(["gpt-5-mini"], 0.84),
        match(["kimi-k3"], 0.85),
        match(["deepseek-v4-pro"], 0.82)
      );
    case "tool_agent_parallel":
      if (terminalToolSignal) {
        return terminalSafetySensitive ? Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["claude-opus-4.8"], 0.9),
          match(["gpt-5.3-codex"], 0.86)
        ) : Math.max(
          base,
          match(["gpt-5-mini"], 1),
          match(["gpt-5.3-codex"], 0.98),
          match(["claude-sonnet-5"], 0.92),
          match(["gemini-3.5-flash"], 0.88)
        );
      }
      if (agentDomain === "web_research") {
        return deepWebResearch ? Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5-mini"], 0.88),
          match(["gemini-3.5-flash"], 0.84),
          match(["claude-opus-5"], 0.8),
          match(["claude-opus-4.8"], 0.78)
        ) : Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5-mini"], 0.88),
          match(["gemini-3.5-flash"], 0.86),
          match(["claude-opus-5"], 0.84),
          match(["claude-opus-4.8"], 0.82)
        );
      }
      if (agentDomain === "retail") {
        if (agentRisk === "policy_exception") {
          return Math.max(
            base,
            match(["gpt-4.1"], 1),
            match(["claude-sonnet-5"], 0.9),
            match(["deepseek-v4-pro"], 0.82),
            match(["gpt-5-mini"], 0.8),
            match(["gpt-4o-mini"], 0.76)
          );
        }
        if (agentRisk === "policy_exception_simple") {
          return Math.max(
            base,
            match(["gpt-5-mini"], 1),
            match(["gpt-4.1"], 0.86),
            match(["deepseek-v4-pro"], 0.82),
            match(["gpt-4o-mini"], 0.8)
          );
        }
        return Math.max(
          base,
          match(["deepseek-v4-pro"], 1),
          match(["claude-sonnet-5"], 0.88),
          match(["claude-opus-4.8"], 0.84),
          match(["gpt-5-mini"], 0.78),
          match(["gemini-3.5-flash"], 0.76)
        );
      }
      if (agentDomain === "airline") {
        return agentRisk === "complex_high" ? Math.max(
          base,
          match(["claude-sonnet-5"], 1),
          match(["gpt-5-mini"], 0.78),
          match(["claude-opus-4.8"], 0.76),
          match(["gemini-3.5-flash"], 0.74)
        ) : Math.max(
          base,
          match(["gpt-5-mini"], 1),
          match(["claude-sonnet-5"], 0.9),
          match(["gemini-3.5-flash"], 0.8)
        );
      }
      return Math.max(
        base,
        match(["claude-opus-4.8"], 1),
        match(["claude-sonnet-5"], 0.84),
        match(["grok-4.5"], 0.82),
        match(["gemini-3.5-flash"], 0.8),
        match(["deepseek-v4-pro"], 0.78)
      );
    case "code_edit":
    case "debug":
      return Math.max(
        base,
        match(["gpt-5.3-codex"], 1),
        match(["claude-sonnet-4.6"], 0.94),
        match(["glm-5.2"], 0.9),
        match(["deepseek-v4-pro"], 0.86)
      );
    case "reasoning":
      return Math.max(
        base,
        match(["claude-sonnet-5", "claude-sonnet-4.6"], 0.98),
        match(["deepseek-v4-pro"], 0.95),
        match(["grok-4.5"], 0.94),
        match(["gemini-3.1-pro", "gemini-3.5-flash"], 0.92)
      );
    case "reasoning_mcq":
      return Math.max(
        base,
        match(["gemini-3-flash-preview"], 1),
        match(["gemini-3.5-flash"], 0.91),
        match(["grok-4.5"], 0.9),
        match(["claude-sonnet-5"], 0.88),
        match(["deepseek-v4-pro"], 0.84)
      );
    case "reasoning_math":
      return Math.max(
        base,
        match(["gemini-3.5-flash"], 1),
        match(["grok-4.5"], 0.93),
        match(["claude-sonnet-5", "deepseek-v4-pro", "kimi-k3"], 0.9)
      );
    case "vision":
      return Math.max(
        base,
        match(["gemini-3.1-pro"], 0.96),
        match(["qwen3.7-max", "claude-sonnet-4.6", "kimi-k3", "grok-4.3"], 0.9)
      );
    case "long_context":
      return Math.max(
        base,
        match(["gemini-3.1-pro"], 1),
        match(["qwen3.7-max", "glm-5.2"], 0.89),
        match(["gemini-3.5-flash"], 0.88),
        match(["deepseek-v4-pro"], 0.85)
      );
    case "extraction": {
      const kimiExtractionAffinity = language === "zh" ? 1 : 0.9;
      const otherExtractionAffinity = language === "zh" ? 0.88 : 0.9;
      return Math.max(
        base,
        match(["gemini-3.5-flash", "gemini-2.5-flash", "gpt-4o-mini"], otherExtractionAffinity),
        match(["claude-sonnet-5", "claude-sonnet-4.6"], otherExtractionAffinity),
        match(["kimi-k3"], kimiExtractionAffinity)
      );
    }
    default:
      return Math.max(
        base,
        match(["gemini-3.5-flash", "gemini-2.5-flash", "kimi-k3"], 0.86)
      );
  }
}
function evidenceCandidates(task) {
  if (task === "code_agent") {
    return [
      "openai/gpt-5.3-codex",
      "anthropic/claude-sonnet-5",
      "openai/gpt-5-mini",
      "google/gemini-3.5-flash",
      "moonshot/kimi-k3",
      "deepseek/deepseek-v4-pro"
    ];
  }
  if (task === "tool_agent") {
    return [
      "anthropic/claude-sonnet-5",
      "anthropic/claude-opus-5",
      "openai/gpt-5-mini",
      "openai/gpt-4.1",
      "openai/gpt-4o-mini",
      "google/gemini-3.5-flash",
      "openai/gpt-5.3-codex",
      "moonshot/kimi-k3",
      "deepseek/deepseek-v4-pro"
    ];
  }
  if (task === "tool_agent_parallel") {
    return [
      "anthropic/claude-opus-5",
      "anthropic/claude-opus-4.8",
      "anthropic/claude-sonnet-5",
      "openai/gpt-5-mini",
      "openai/gpt-4.1",
      "openai/gpt-4o-mini",
      "xai/grok-4.5",
      "google/gemini-3.5-flash",
      "deepseek/deepseek-v4-pro"
    ];
  }
  if (task === "long_context") {
    return [
      "google/gemini-3.1-pro",
      "deepseek/deepseek-v4-pro",
      "qwen/qwen3.7-max",
      "zai/glm-5.2",
      "google/gemini-3.5-flash"
    ];
  }
  if (task === "reasoning_mcq") {
    return [
      "google/gemini-3-flash-preview",
      "google/gemini-3.5-flash",
      "xai/grok-4.5",
      "anthropic/claude-sonnet-5",
      "deepseek/deepseek-v4-pro"
    ];
  }
  if (task === "extraction") {
    return ["moonshot/kimi-k3", "google/gemini-3.5-flash", "anthropic/claude-sonnet-5"];
  }
  if (task === "reasoning_math") {
    return [
      "google/gemini-3.5-flash",
      "xai/grok-4.5",
      "anthropic/claude-sonnet-5",
      "deepseek/deepseek-v4-pro",
      "moonshot/kimi-k3"
    ];
  }
  return [];
}
var VERIFIED_TOOL_FALLBACKS = [
  "anthropic/claude-fable-5.1",
  "anthropic/claude-opus-5.5",
  "anthropic/claude-sonnet-5.5",
  "deepseek/deepseek-v4.1-flash",
  "google/gemini-3.8-flash",
  "openai/gpt-5.1",
  "openai/gpt-6-astra",
  "openai/gpt-6-luna",
  "openai/gpt-6-sol",
  "openai/gpt-6.1-sol",
  "xai/grok-4.6",
  "xai/grok-4.7",
  "xiaomi/mimo-v2.5"
];
function isEligible(modelId, features, maxOutputTokens, options) {
  const model = options.modelCapabilities?.[modelId] ?? DEFAULT_MODEL_CAPABILITIES[modelId];
  if (!model) return true;
  if (features.needsTools && !model.supportsTools) return false;
  if (features.needsVision && !model.supportsVision) return false;
  if (features.needsStructuredOutput && !model.supportsTools) return false;
  if (model.maxOutputTokens < maxOutputTokens) return false;
  return model.contextWindow >= (features.estimatedInputTokens + maxOutputTokens) * 1.1;
}
function estimatedCost(modelId, options, inputTokens, outputTokens) {
  const price2 = options.modelPricing.get(modelId);
  if (!price2) return Number.POSITIVE_INFINITY;
  if (price2.flatPrice !== void 0) return price2.flatPrice;
  return (inputTokens * price2.inputPrice + outputTokens * price2.outputPrice) / 1e6;
}
function profileScore(modelId, options, now) {
  const profile = options.modelPerformance?.[modelId] ?? LIVE_MODEL_PROFILES[modelId] ?? HISTORICAL_MODEL_PROFILES[modelId];
  if (!profile) return void 0;
  const measuredAt = Date.parse(profile.measuredAt);
  if (!Number.isFinite(measuredAt)) return void 0;
  const ageDays = Math.max(0, (now.getTime() - measuredAt) / 864e5);
  const sampleConfidence = profile.samples === void 0 ? 1 : Math.min(1, Math.max(0, profile.samples) / 10);
  const freshness = Math.pow(0.5, ageDays / 30) * sampleConfidence;
  const quality = profile.intelligenceIndex === void 0 ? void 0 : Math.min(1, profile.intelligenceIndex / 50);
  const speed = Math.min(
    1,
    (2e3 / Math.max(500, profile.latencyMs) + profile.outputTokensPerSecond / 250) / 2
  );
  const tailSpeed = Math.min(1, 3e3 / Math.max(750, profile.p95LatencyMs ?? profile.latencyMs));
  const reliability = Math.max(0, 1 - (profile.errorRate ?? 0));
  return { quality, speed, tailSpeed, reliability, freshness };
}
var PortfolioStrategy = class {
  name = "portfolio";
  // Rules set the capability tier; the strategy is stateless, so one instance serves every request.
  rules = new RulesStrategy();
  route(prompt, systemPrompt, maxOutputTokens, options) {
    const features = classifyTask(prompt, systemPrompt, options);
    const base = this.rules.route(prompt, systemPrompt, maxOutputTokens, {
      ...options,
      requiresTools: features.needsTools
    });
    const tierConfigs = base.tierConfigs;
    if (!tierConfigs) return base;
    const targetTier = (features.taskType === "reasoning_mcq" || features.taskType === "reasoning_math") && (base.tier === "SIMPLE" || base.tier === "MEDIUM") ? "REASONING" : (features.taskType === "debug" || features.taskType === "code_edit") && features.codeArtifact && base.tier === "SIMPLE" ? "MEDIUM" : base.tier;
    const tierConfig = tierConfigs[targetTier];
    const configuredCandidates = tierConfig ? getFallbackChain(targetTier, tierConfigs) : [];
    const unavailable = new Set(options.unavailableModels ?? []);
    const verifiedToolFallbacks = features.needsTools ? VERIFIED_TOOL_FALLBACKS.filter((model2) => options.modelPricing.has(model2)) : [];
    if (options.routingProfile !== "premium") {
      const fallbackCost = (model2) => estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
      verifiedToolFallbacks.sort((a, b) => fallbackCost(a) - fallbackCost(b));
    }
    const chain = [
      .../* @__PURE__ */ new Set([
        ...configuredCandidates,
        ...evidenceCandidates(features.taskType),
        ...verifiedToolFallbacks
      ])
    ].filter(
      (model2) => typeof model2 === "string" && model2.length > 0 && !unavailable.has(model2)
    );
    const eligible = new Set(
      chain.filter((model2) => isEligible(model2, features, maxOutputTokens, options))
    );
    const eligibleCandidates = eligible.size > 0 ? [...eligible] : chain;
    if (eligibleCandidates.length === 0) return base;
    const profileName = options.routingProfile === "eco" ? "eco" : options.routingProfile === "premium" ? "premium" : "auto";
    const portfolio = options.config.portfolio ?? DEFAULT_PORTFOLIO_WEIGHTS;
    const getAffinity = (model2) => affinity(
      model2,
      features.taskType,
      features.language,
      features.agentDomain,
      features.deepWebResearch,
      features.agentRisk,
      features.terminalToolSignal,
      features.terminalSafetySensitive
    );
    const bestAffinity = Math.max(...eligibleCandidates.map(getAffinity));
    const specificAffinity = eligibleCandidates.filter((model2) => getAffinity(model2) > 0.68);
    const affinityPool = specificAffinity.length > 0 ? specificAffinity : [eligibleCandidates[0]];
    const affinityFloorGap = features.terminalToolSignal ? Math.max(
      portfolio.affinityFloorGap[profileName],
      features.terminalSafetySensitive ? 0.15 : 0.12
    ) : portfolio.affinityFloorGap[profileName];
    const candidates = affinityPool.filter(
      (model2) => getAffinity(model2) >= bestAffinity - affinityFloorGap
    );
    const costs = candidates.map(
      (model2) => estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens)
    );
    const finiteCosts = costs.filter(Number.isFinite);
    const minCost = finiteCosts.length > 0 ? Math.min(...finiteCosts) : 0;
    const maxCost = finiteCosts.length > 0 ? Math.max(...finiteCosts) : 1;
    const now = options.now ?? /* @__PURE__ */ new Date();
    const profileWeights = portfolio[profileName];
    const rankedEntries = candidates.map((model2, index) => {
      const cost = estimatedCost(model2, options, features.estimatedInputTokens, maxOutputTokens);
      const costScore = Number.isFinite(cost) && maxCost > minCost ? 1 - (cost - minCost) / (maxCost - minCost) : 0.5;
      const capabilityScore = eligible.has(model2) ? 1 : 0;
      const profile = profileScore(model2, options, now);
      const observedQuality = profile?.quality === void 0 ? getAffinity(model2) : getAffinity(model2) * (1 - profile.freshness) + profile.quality * profile.freshness;
      const observedSpeed = profile ? profile.speed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
      const observedTailSpeed = profile ? profile.tailSpeed * profile.freshness + 0.5 * (1 - profile.freshness) : 0.5;
      const observedReliability = profile ? profile.reliability * profile.freshness + (1 - profile.freshness) : 1;
      const legacyScore = 1 - index / Math.max(1, candidates.length - 1);
      const qualityWeight = profileWeights.quality + (features.highStakes ? portfolio.highStakesBoost.quality : 0);
      const speedScore = features.latencySensitive ? observedTailSpeed : observedSpeed;
      const speedWeight = profileWeights.speed + (features.latencySensitive ? portfolio.latencySensitiveSpeedBoost : 0);
      const reliabilityWeight = profileWeights.reliability + (features.highStakes ? portfolio.highStakesBoost.reliability : 0);
      const score = observedQuality * qualityWeight + capabilityScore * profileWeights.capability + costScore * profileWeights.cost + speedScore * speedWeight + observedReliability * reliabilityWeight + legacyScore * profileWeights.legacy;
      return {
        model: model2,
        score,
        quality: observedQuality,
        cost: costScore,
        speed: speedScore,
        reliability: observedReliability
      };
    }).sort((a, b) => b.score - a.score);
    const scoredModels = rankedEntries.map((item) => item.model);
    const webResearchFallbackOrder = [
      "anthropic/claude-sonnet-5",
      "openai/gpt-5-mini",
      "google/gemini-3.5-flash",
      "anthropic/claude-opus-5",
      "anthropic/claude-opus-4.8",
      "openai/gpt-5.3-codex"
    ];
    const ranked = features.agentDomain === "web_research" ? [
      ...scoredModels,
      ...webResearchFallbackOrder.filter(
        (model2) => eligibleCandidates.includes(model2) && !scoredModels.includes(model2)
      ),
      ...eligibleCandidates.filter(
        (model2) => !scoredModels.includes(model2) && !webResearchFallbackOrder.includes(model2)
      )
    ] : [...scoredModels, ...eligibleCandidates.filter((model2) => !scoredModels.includes(model2))];
    const model = ranked[0] ?? base.model;
    const selectedTierConfigs = {
      ...tierConfigs,
      [targetTier]: { primary: model, fallback: ranked.slice(1) }
    };
    const decision = selectModel(
      targetTier,
      base.confidence,
      "portfolio",
      `${base.reasoning} | v3 task=${features.taskType} agentRisk=${features.agentRisk} deepWebResearch=${features.deepWebResearch} terminalCode=${features.implicitTerminalCode} terminalSafety=${features.terminalSafetySensitive} candidates=${ranked.length}`,
      selectedTierConfigs,
      options.modelPricing,
      features.estimatedInputTokens,
      maxOutputTokens,
      options.routingProfile,
      base.agenticScore
    );
    return {
      ...decision,
      tierConfigs: selectedTierConfigs,
      profile: base.profile,
      candidates: ranked,
      candidateScores: rankedEntries.map(({ model: model2, score, quality, cost, speed, reliability }) => ({
        model: model2,
        score,
        quality,
        cost,
        speed,
        reliability
      })),
      taskType: features.taskType,
      routerVersion: "v3-portfolio"
    };
  }
};
var DEFAULT_ROUTING_CONFIG = {
  version: "3.5",
  strategy: "portfolio",
  portfolio: {
    auto: {
      quality: 0.47,
      capability: 0.2,
      cost: 0.18,
      speed: 0.07,
      reliability: 0.03,
      legacy: 0.05
    },
    eco: {
      quality: 0.36,
      capability: 0.2,
      cost: 0.28,
      speed: 0.1,
      reliability: 0.04,
      legacy: 0.02
    },
    premium: {
      quality: 0.58,
      capability: 0.2,
      cost: 0.08,
      speed: 0.06,
      reliability: 0.06,
      legacy: 0.02
    },
    highStakesBoost: { quality: 0.08, reliability: 0.05 },
    latencySensitiveSpeedBoost: 0.08,
    affinityFloorGap: { auto: 0.1, eco: 0.22, premium: 0.05 }
  },
  classifier: {
    llmModel: "google/gemini-2.5-flash",
    llmMaxTokens: 10,
    llmTemperature: 0,
    promptTruncationChars: 500,
    cacheTtlMs: 36e5
    // 1 hour
  },
  scoring: {
    tokenCountThresholds: { simple: 50, complex: 500 },
    // Multilingual keywords: EN + ZH + JA + RU + DE + ES + PT + KO + AR
    codeKeywords: [
      // English
      "function",
      "class",
      "import",
      "def",
      "SELECT",
      "async",
      "await",
      "const",
      "let",
      "var",
      "return",
      "```",
      // Chinese
      "\u51FD\u6570",
      "\u7C7B",
      "\u5BFC\u5165",
      "\u5B9A\u4E49",
      "\u67E5\u8BE2",
      "\u5F02\u6B65",
      "\u7B49\u5F85",
      "\u5E38\u91CF",
      "\u53D8\u91CF",
      "\u8FD4\u56DE",
      // Japanese
      "\u95A2\u6570",
      "\u30AF\u30E9\u30B9",
      "\u30A4\u30F3\u30DD\u30FC\u30C8",
      "\u975E\u540C\u671F",
      "\u5B9A\u6570",
      "\u5909\u6570",
      // Russian
      "\u0444\u0443\u043D\u043A\u0446\u0438\u044F",
      "\u043A\u043B\u0430\u0441\u0441",
      "\u0438\u043C\u043F\u043E\u0440\u0442",
      "\u043E\u043F\u0440\u0435\u0434\u0435\u043B",
      "\u0437\u0430\u043F\u0440\u043E\u0441",
      "\u0430\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u043D\u044B\u0439",
      "\u043E\u0436\u0438\u0434\u0430\u0442\u044C",
      "\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u0430",
      "\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F",
      "\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
      // German
      "funktion",
      "klasse",
      "importieren",
      "definieren",
      "abfrage",
      "asynchron",
      "erwarten",
      "konstante",
      "variable",
      "zur\xFCckgeben",
      // Spanish
      "funci\xF3n",
      "clase",
      "importar",
      "definir",
      "consulta",
      "as\xEDncrono",
      "esperar",
      "constante",
      "variable",
      "retornar",
      // Portuguese
      "fun\xE7\xE3o",
      "classe",
      "importar",
      "definir",
      "consulta",
      "ass\xEDncrono",
      "aguardar",
      "constante",
      "vari\xE1vel",
      "retornar",
      // Korean
      "\uD568\uC218",
      "\uD074\uB798\uC2A4",
      "\uAC00\uC838\uC624\uAE30",
      "\uC815\uC758",
      "\uCFFC\uB9AC",
      "\uBE44\uB3D9\uAE30",
      "\uB300\uAE30",
      "\uC0C1\uC218",
      "\uBCC0\uC218",
      "\uBC18\uD658",
      // Arabic
      "\u062F\u0627\u0644\u0629",
      "\u0641\u0626\u0629",
      "\u0627\u0633\u062A\u064A\u0631\u0627\u062F",
      "\u062A\u0639\u0631\u064A\u0641",
      "\u0627\u0633\u062A\u0639\u0644\u0627\u0645",
      "\u063A\u064A\u0631 \u0645\u062A\u0632\u0627\u0645\u0646",
      "\u0627\u0646\u062A\u0638\u0627\u0631",
      "\u062B\u0627\u0628\u062A",
      "\u0645\u062A\u063A\u064A\u0631",
      "\u0625\u0631\u062C\u0627\u0639"
    ],
    reasoningKeywords: [
      // English
      "prove",
      "theorem",
      "derive",
      "step by step",
      "chain of thought",
      "formally",
      "mathematical",
      "proof",
      "logically",
      // Chinese
      "\u8BC1\u660E",
      "\u5B9A\u7406",
      "\u63A8\u5BFC",
      "\u9010\u6B65",
      "\u601D\u7EF4\u94FE",
      "\u5F62\u5F0F\u5316",
      "\u6570\u5B66",
      "\u903B\u8F91",
      // Japanese
      "\u8A3C\u660E",
      "\u5B9A\u7406",
      "\u5C0E\u51FA",
      "\u30B9\u30C6\u30C3\u30D7\u30D0\u30A4\u30B9\u30C6\u30C3\u30D7",
      "\u8AD6\u7406\u7684",
      // Russian
      "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u044C",
      "\u0434\u043E\u043A\u0430\u0436\u0438",
      "\u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432",
      "\u0442\u0435\u043E\u0440\u0435\u043C\u0430",
      "\u0432\u044B\u0432\u0435\u0441\u0442\u0438",
      "\u0448\u0430\u0433 \u0437\u0430 \u0448\u0430\u0433\u043E\u043C",
      "\u043F\u043E\u0448\u0430\u0433\u043E\u0432\u043E",
      "\u043F\u043E\u044D\u0442\u0430\u043F\u043D\u043E",
      "\u0446\u0435\u043F\u043E\u0447\u043A\u0430 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439",
      "\u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438",
      "\u0444\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u043E",
      "\u043C\u0430\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438",
      "\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438",
      // German
      "beweisen",
      "beweis",
      "theorem",
      "ableiten",
      "schritt f\xFCr schritt",
      "gedankenkette",
      "formal",
      "mathematisch",
      "logisch",
      // Spanish
      "demostrar",
      "teorema",
      "derivar",
      "paso a paso",
      "cadena de pensamiento",
      "formalmente",
      "matem\xE1tico",
      "prueba",
      "l\xF3gicamente",
      // Portuguese
      "provar",
      "teorema",
      "derivar",
      "passo a passo",
      "cadeia de pensamento",
      "formalmente",
      "matem\xE1tico",
      "prova",
      "logicamente",
      // Korean
      "\uC99D\uBA85",
      "\uC815\uB9AC",
      "\uB3C4\uCD9C",
      "\uB2E8\uACC4\uBCC4",
      "\uC0AC\uACE0\uC758 \uC5F0\uC1C4",
      "\uD615\uC2DD\uC801",
      "\uC218\uD559\uC801",
      "\uB17C\uB9AC\uC801",
      // Arabic
      "\u0625\u062B\u0628\u0627\u062A",
      "\u0646\u0638\u0631\u064A\u0629",
      "\u0627\u0634\u062A\u0642\u0627\u0642",
      "\u062E\u0637\u0648\u0629 \u0628\u062E\u0637\u0648\u0629",
      "\u0633\u0644\u0633\u0644\u0629 \u0627\u0644\u062A\u0641\u0643\u064A\u0631",
      "\u0631\u0633\u0645\u064A\u0627\u064B",
      "\u0631\u064A\u0627\u0636\u064A",
      "\u0628\u0631\u0647\u0627\u0646",
      "\u0645\u0646\u0637\u0642\u064A\u0627\u064B"
    ],
    simpleKeywords: [
      // English
      "what is",
      "define",
      "translate",
      "hello",
      "yes or no",
      "capital of",
      "how old",
      "who is",
      "when was",
      // Chinese
      "\u4EC0\u4E48\u662F",
      "\u5B9A\u4E49",
      "\u7FFB\u8BD1",
      "\u4F60\u597D",
      "\u662F\u5426",
      "\u9996\u90FD",
      "\u591A\u5927",
      "\u8C01\u662F",
      "\u4F55\u65F6",
      // Japanese
      "\u3068\u306F",
      "\u5B9A\u7FA9",
      "\u7FFB\u8A33",
      "\u3053\u3093\u306B\u3061\u306F",
      "\u306F\u3044\u304B\u3044\u3044\u3048",
      "\u9996\u90FD",
      "\u8AB0",
      // Russian
      "\u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0435",
      "\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435",
      "\u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438",
      "\u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0438",
      "\u043F\u0440\u0438\u0432\u0435\u0442",
      "\u0434\u0430 \u0438\u043B\u0438 \u043D\u0435\u0442",
      "\u0441\u0442\u043E\u043B\u0438\u0446\u0430",
      "\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043B\u0435\u0442",
      "\u043A\u0442\u043E \u0442\u0430\u043A\u043E\u0439",
      "\u043A\u043E\u0433\u0434\u0430",
      "\u043E\u0431\u044A\u044F\u0441\u043D\u0438",
      // German
      "was ist",
      "definiere",
      "\xFCbersetze",
      "hallo",
      "ja oder nein",
      "hauptstadt",
      "wie alt",
      "wer ist",
      "wann",
      "erkl\xE4re",
      // Spanish
      "qu\xE9 es",
      "definir",
      "traducir",
      "hola",
      "s\xED o no",
      "capital de",
      "cu\xE1ntos a\xF1os",
      "qui\xE9n es",
      "cu\xE1ndo",
      // Portuguese
      "o que \xE9",
      "definir",
      "traduzir",
      "ol\xE1",
      "sim ou n\xE3o",
      "capital de",
      "quantos anos",
      "quem \xE9",
      "quando",
      // Korean
      "\uBB34\uC5C7",
      "\uC815\uC758",
      "\uBC88\uC5ED",
      "\uC548\uB155\uD558\uC138\uC694",
      "\uC608 \uB610\uB294 \uC544\uB2C8\uC624",
      "\uC218\uB3C4",
      "\uB204\uAD6C",
      "\uC5B8\uC81C",
      // Arabic
      "\u0645\u0627 \u0647\u0648",
      "\u062A\u0639\u0631\u064A\u0641",
      "\u062A\u0631\u062C\u0645",
      "\u0645\u0631\u062D\u0628\u0627",
      "\u0646\u0639\u0645 \u0623\u0648 \u0644\u0627",
      "\u0639\u0627\u0635\u0645\u0629",
      "\u0645\u0646 \u0647\u0648",
      "\u0645\u062A\u0649"
    ],
    technicalKeywords: [
      // English
      "algorithm",
      "optimize",
      "architecture",
      "distributed",
      "kubernetes",
      "microservice",
      "database",
      "infrastructure",
      // Chinese
      "\u7B97\u6CD5",
      "\u4F18\u5316",
      "\u67B6\u6784",
      "\u5206\u5E03\u5F0F",
      "\u5FAE\u670D\u52A1",
      "\u6570\u636E\u5E93",
      "\u57FA\u7840\u8BBE\u65BD",
      // Japanese
      "\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0",
      "\u6700\u9069\u5316",
      "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3",
      "\u5206\u6563",
      "\u30DE\u30A4\u30AF\u30ED\u30B5\u30FC\u30D3\u30B9",
      "\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9",
      // Russian
      "\u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C",
      "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
      "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438",
      "\u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u0443\u0439",
      "\u0430\u0440\u0445\u0438\u0442\u0435\u043A\u0442\u0443\u0440\u0430",
      "\u0440\u0430\u0441\u043F\u0440\u0435\u0434\u0435\u043B\u0451\u043D\u043D\u044B\u0439",
      "\u043C\u0438\u043A\u0440\u043E\u0441\u0435\u0440\u0432\u0438\u0441",
      "\u0431\u0430\u0437\u0430 \u0434\u0430\u043D\u043D\u044B\u0445",
      "\u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0430",
      // German
      "algorithmus",
      "optimieren",
      "architektur",
      "verteilt",
      "kubernetes",
      "mikroservice",
      "datenbank",
      "infrastruktur",
      // Spanish
      "algoritmo",
      "optimizar",
      "arquitectura",
      "distribuido",
      "microservicio",
      "base de datos",
      "infraestructura",
      // Portuguese
      "algoritmo",
      "otimizar",
      "arquitetura",
      "distribu\xEDdo",
      "microsservi\xE7o",
      "banco de dados",
      "infraestrutura",
      // Korean
      "\uC54C\uACE0\uB9AC\uC998",
      "\uCD5C\uC801\uD654",
      "\uC544\uD0A4\uD14D\uCC98",
      "\uBD84\uC0B0",
      "\uB9C8\uC774\uD06C\uB85C\uC11C\uBE44\uC2A4",
      "\uB370\uC774\uD130\uBCA0\uC774\uC2A4",
      "\uC778\uD504\uB77C",
      // Arabic
      "\u062E\u0648\u0627\u0631\u0632\u0645\u064A\u0629",
      "\u062A\u062D\u0633\u064A\u0646",
      "\u0628\u0646\u064A\u0629",
      "\u0645\u0648\u0632\u0639",
      "\u062E\u062F\u0645\u0629 \u0645\u0635\u063A\u0631\u0629",
      "\u0642\u0627\u0639\u062F\u0629 \u0628\u064A\u0627\u0646\u0627\u062A",
      "\u0628\u0646\u064A\u0629 \u062A\u062D\u062A\u064A\u0629"
    ],
    creativeKeywords: [
      // English
      "story",
      "poem",
      "compose",
      "brainstorm",
      "creative",
      "imagine",
      "write a",
      // Chinese
      "\u6545\u4E8B",
      "\u8BD7",
      "\u521B\u4F5C",
      "\u5934\u8111\u98CE\u66B4",
      "\u521B\u610F",
      "\u60F3\u8C61",
      "\u5199\u4E00\u4E2A",
      // Japanese
      "\u7269\u8A9E",
      "\u8A69",
      "\u4F5C\u66F2",
      "\u30D6\u30EC\u30A4\u30F3\u30B9\u30C8\u30FC\u30E0",
      "\u5275\u9020\u7684",
      "\u60F3\u50CF",
      // Russian
      "\u0438\u0441\u0442\u043E\u0440\u0438\u044F",
      "\u0440\u0430\u0441\u0441\u043A\u0430\u0437",
      "\u0441\u0442\u0438\u0445\u043E\u0442\u0432\u043E\u0440\u0435\u043D\u0438\u0435",
      "\u0441\u043E\u0447\u0438\u043D\u0438\u0442\u044C",
      "\u0441\u043E\u0447\u0438\u043D\u0438",
      "\u043C\u043E\u0437\u0433\u043E\u0432\u043E\u0439 \u0448\u0442\u0443\u0440\u043C",
      "\u0442\u0432\u043E\u0440\u0447\u0435\u0441\u043A\u0438\u0439",
      "\u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044C",
      "\u043F\u0440\u0438\u0434\u0443\u043C\u0430\u0439",
      "\u043D\u0430\u043F\u0438\u0448\u0438",
      // German
      "geschichte",
      "gedicht",
      "komponieren",
      "brainstorming",
      "kreativ",
      "vorstellen",
      "schreibe",
      "erz\xE4hlung",
      // Spanish
      "historia",
      "poema",
      "componer",
      "lluvia de ideas",
      "creativo",
      "imaginar",
      "escribe",
      // Portuguese
      "hist\xF3ria",
      "poema",
      "compor",
      "criativo",
      "imaginar",
      "escreva",
      // Korean
      "\uC774\uC57C\uAE30",
      "\uC2DC",
      "\uC791\uACE1",
      "\uBE0C\uB808\uC778\uC2A4\uD1A0\uBC0D",
      "\uCC3D\uC758\uC801",
      "\uC0C1\uC0C1",
      "\uC791\uC131",
      // Arabic
      "\u0642\u0635\u0629",
      "\u0642\u0635\u064A\u062F\u0629",
      "\u062A\u0623\u0644\u064A\u0641",
      "\u0639\u0635\u0641 \u0630\u0647\u0646\u064A",
      "\u0625\u0628\u062F\u0627\u0639\u064A",
      "\u062A\u062E\u064A\u0644",
      "\u0627\u0643\u062A\u0628"
    ],
    // New dimension keyword lists (multilingual)
    imperativeVerbs: [
      // English
      "build",
      "create",
      "implement",
      "design",
      "develop",
      "construct",
      "generate",
      "deploy",
      "configure",
      "set up",
      // Chinese
      "\u6784\u5EFA",
      "\u521B\u5EFA",
      "\u5B9E\u73B0",
      "\u8BBE\u8BA1",
      "\u5F00\u53D1",
      "\u751F\u6210",
      "\u90E8\u7F72",
      "\u914D\u7F6E",
      "\u8BBE\u7F6E",
      // Japanese
      "\u69CB\u7BC9",
      "\u4F5C\u6210",
      "\u5B9F\u88C5",
      "\u8A2D\u8A08",
      "\u958B\u767A",
      "\u751F\u6210",
      "\u30C7\u30D7\u30ED\u30A4",
      "\u8A2D\u5B9A",
      // Russian
      "\u043F\u043E\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
      "\u043F\u043E\u0441\u0442\u0440\u043E\u0439",
      "\u0441\u043E\u0437\u0434\u0430\u0442\u044C",
      "\u0441\u043E\u0437\u0434\u0430\u0439",
      "\u0440\u0435\u0430\u043B\u0438\u0437\u043E\u0432\u0430\u0442\u044C",
      "\u0440\u0435\u0430\u043B\u0438\u0437\u0443\u0439",
      "\u0441\u043F\u0440\u043E\u0435\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
      "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C",
      "\u0440\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0430\u0439",
      "\u0441\u043A\u043E\u043D\u0441\u0442\u0440\u0443\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
      "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
      "\u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439",
      "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
      "\u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0438",
      "\u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C",
      "\u043D\u0430\u0441\u0442\u0440\u043E\u0439",
      // German
      "erstellen",
      "bauen",
      "implementieren",
      "entwerfen",
      "entwickeln",
      "konstruieren",
      "generieren",
      "bereitstellen",
      "konfigurieren",
      "einrichten",
      // Spanish
      "construir",
      "crear",
      "implementar",
      "dise\xF1ar",
      "desarrollar",
      "generar",
      "desplegar",
      "configurar",
      // Portuguese
      "construir",
      "criar",
      "implementar",
      "projetar",
      "desenvolver",
      "gerar",
      "implantar",
      "configurar",
      // Korean
      "\uAD6C\uCD95",
      "\uC0DD\uC131",
      "\uAD6C\uD604",
      "\uC124\uACC4",
      "\uAC1C\uBC1C",
      "\uBC30\uD3EC",
      "\uC124\uC815",
      // Arabic
      "\u0628\u0646\u0627\u0621",
      "\u0625\u0646\u0634\u0627\u0621",
      "\u062A\u0646\u0641\u064A\u0630",
      "\u062A\u0635\u0645\u064A\u0645",
      "\u062A\u0637\u0648\u064A\u0631",
      "\u062A\u0648\u0644\u064A\u062F",
      "\u0646\u0634\u0631",
      "\u0625\u0639\u062F\u0627\u062F"
    ],
    constraintIndicators: [
      // English
      "under",
      "at most",
      "at least",
      "within",
      "no more than",
      "o(",
      "maximum",
      "minimum",
      "limit",
      "budget",
      // Chinese
      "\u4E0D\u8D85\u8FC7",
      "\u81F3\u5C11",
      "\u6700\u591A",
      "\u5728\u5185",
      "\u6700\u5927",
      "\u6700\u5C0F",
      "\u9650\u5236",
      "\u9884\u7B97",
      // Japanese
      "\u4EE5\u4E0B",
      "\u6700\u5927",
      "\u6700\u5C0F",
      "\u5236\u9650",
      "\u4E88\u7B97",
      // Russian
      "\u043D\u0435 \u0431\u043E\u043B\u0435\u0435",
      "\u043D\u0435 \u043C\u0435\u043D\u0435\u0435",
      "\u043A\u0430\u043A \u043C\u0438\u043D\u0438\u043C\u0443\u043C",
      "\u0432 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u0445",
      "\u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C",
      "\u043C\u0438\u043D\u0438\u043C\u0443\u043C",
      "\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435",
      "\u0431\u044E\u0434\u0436\u0435\u0442",
      // German
      "h\xF6chstens",
      "mindestens",
      "innerhalb",
      "nicht mehr als",
      "maximal",
      "minimal",
      "grenze",
      "budget",
      // Spanish
      "como m\xE1ximo",
      "al menos",
      "dentro de",
      "no m\xE1s de",
      "m\xE1ximo",
      "m\xEDnimo",
      "l\xEDmite",
      "presupuesto",
      // Portuguese
      "no m\xE1ximo",
      "pelo menos",
      "dentro de",
      "n\xE3o mais que",
      "m\xE1ximo",
      "m\xEDnimo",
      "limite",
      "or\xE7amento",
      // Korean
      "\uC774\uD558",
      "\uC774\uC0C1",
      "\uCD5C\uB300",
      "\uCD5C\uC18C",
      "\uC81C\uD55C",
      "\uC608\uC0B0",
      // Arabic
      "\u0639\u0644\u0649 \u0627\u0644\u0623\u0643\u062B\u0631",
      "\u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644",
      "\u0636\u0645\u0646",
      "\u0644\u0627 \u064A\u0632\u064A\u062F \u0639\u0646",
      "\u0623\u0642\u0635\u0649",
      "\u0623\u062F\u0646\u0649",
      "\u062D\u062F",
      "\u0645\u064A\u0632\u0627\u0646\u064A\u0629"
    ],
    outputFormatKeywords: [
      // English
      "json",
      "yaml",
      "xml",
      "table",
      "csv",
      "markdown",
      "schema",
      "format as",
      "structured",
      // Chinese
      "\u8868\u683C",
      "\u683C\u5F0F\u5316\u4E3A",
      "\u7ED3\u6784\u5316",
      // Japanese
      "\u30C6\u30FC\u30D6\u30EB",
      "\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8",
      "\u69CB\u9020\u5316",
      // Russian
      "\u0442\u0430\u0431\u043B\u0438\u0446\u0430",
      "\u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u043A",
      "\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439",
      // German
      "tabelle",
      "formatieren als",
      "strukturiert",
      // Spanish
      "tabla",
      "formatear como",
      "estructurado",
      // Portuguese
      "tabela",
      "formatar como",
      "estruturado",
      // Korean
      "\uD14C\uC774\uBE14",
      "\uD615\uC2DD",
      "\uAD6C\uC870\uD654",
      // Arabic
      "\u062C\u062F\u0648\u0644",
      "\u062A\u0646\u0633\u064A\u0642",
      "\u0645\u0646\u0638\u0645"
    ],
    referenceKeywords: [
      // English
      "above",
      "below",
      "previous",
      "following",
      "the docs",
      "the api",
      "the code",
      "earlier",
      "attached",
      // Chinese
      "\u4E0A\u9762",
      "\u4E0B\u9762",
      "\u4E4B\u524D",
      "\u63A5\u4E0B\u6765",
      "\u6587\u6863",
      "\u4EE3\u7801",
      "\u9644\u4EF6",
      // Japanese
      "\u4E0A\u8A18",
      "\u4E0B\u8A18",
      "\u524D\u306E",
      "\u6B21\u306E",
      "\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8",
      "\u30B3\u30FC\u30C9",
      // Russian
      "\u0432\u044B\u0448\u0435",
      "\u043D\u0438\u0436\u0435",
      "\u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0439",
      "\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439",
      "\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044F",
      "\u043A\u043E\u0434",
      "\u0440\u0430\u043D\u0435\u0435",
      "\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
      // German
      "oben",
      "unten",
      "vorherige",
      "folgende",
      "dokumentation",
      "der code",
      "fr\xFCher",
      "anhang",
      // Spanish
      "arriba",
      "abajo",
      "anterior",
      "siguiente",
      "documentaci\xF3n",
      "el c\xF3digo",
      "adjunto",
      // Portuguese
      "acima",
      "abaixo",
      "anterior",
      "seguinte",
      "documenta\xE7\xE3o",
      "o c\xF3digo",
      "anexo",
      // Korean
      "\uC704",
      "\uC544\uB798",
      "\uC774\uC804",
      "\uB2E4\uC74C",
      "\uBB38\uC11C",
      "\uCF54\uB4DC",
      "\uCCA8\uBD80",
      // Arabic
      "\u0623\u0639\u0644\u0627\u0647",
      "\u0623\u062F\u0646\u0627\u0647",
      "\u0627\u0644\u0633\u0627\u0628\u0642",
      "\u0627\u0644\u062A\u0627\u0644\u064A",
      "\u0627\u0644\u0648\u062B\u0627\u0626\u0642",
      "\u0627\u0644\u0643\u0648\u062F",
      "\u0645\u0631\u0641\u0642"
    ],
    negationKeywords: [
      // English
      "don't",
      "do not",
      "avoid",
      "never",
      "without",
      "except",
      "exclude",
      "no longer",
      // Chinese
      "\u4E0D\u8981",
      "\u907F\u514D",
      "\u4ECE\u4E0D",
      "\u6CA1\u6709",
      "\u9664\u4E86",
      "\u6392\u9664",
      // Japanese
      "\u3057\u306A\u3044\u3067",
      "\u907F\u3051\u308B",
      "\u6C7A\u3057\u3066",
      "\u306A\u3057\u3067",
      "\u9664\u304F",
      // Russian
      "\u043D\u0435 \u0434\u0435\u043B\u0430\u0439",
      "\u043D\u0435 \u043D\u0430\u0434\u043E",
      "\u043D\u0435\u043B\u044C\u0437\u044F",
      "\u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044C",
      "\u043D\u0438\u043A\u043E\u0433\u0434\u0430",
      "\u0431\u0435\u0437",
      "\u043A\u0440\u043E\u043C\u0435",
      "\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C",
      "\u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435",
      // German
      "nicht",
      "vermeide",
      "niemals",
      "ohne",
      "au\xDFer",
      "ausschlie\xDFen",
      "nicht mehr",
      // Spanish
      "no hagas",
      "evitar",
      "nunca",
      "sin",
      "excepto",
      "excluir",
      // Portuguese
      "n\xE3o fa\xE7a",
      "evitar",
      "nunca",
      "sem",
      "exceto",
      "excluir",
      // Korean
      "\uD558\uC9C0 \uB9C8",
      "\uD53C\uD558\uB2E4",
      "\uC808\uB300",
      "\uC5C6\uC774",
      "\uC81C\uC678",
      // Arabic
      "\u0644\u0627 \u062A\u0641\u0639\u0644",
      "\u062A\u062C\u0646\u0628",
      "\u0623\u0628\u062F\u0627\u064B",
      "\u0628\u062F\u0648\u0646",
      "\u0628\u0627\u0633\u062A\u062B\u0646\u0627\u0621",
      "\u0627\u0633\u062A\u0628\u0639\u0627\u062F"
    ],
    domainSpecificKeywords: [
      // English
      "quantum",
      "fpga",
      "vlsi",
      "risc-v",
      "asic",
      "photonics",
      "genomics",
      "proteomics",
      "topological",
      "homomorphic",
      "zero-knowledge",
      "lattice-based",
      // Chinese
      "\u91CF\u5B50",
      "\u5149\u5B50\u5B66",
      "\u57FA\u56E0\u7EC4\u5B66",
      "\u86CB\u767D\u8D28\u7EC4\u5B66",
      "\u62D3\u6251",
      "\u540C\u6001",
      "\u96F6\u77E5\u8BC6",
      "\u683C\u5BC6\u7801",
      // Japanese
      "\u91CF\u5B50",
      "\u30D5\u30A9\u30C8\u30CB\u30AF\u30B9",
      "\u30B2\u30CE\u30DF\u30AF\u30B9",
      "\u30C8\u30DD\u30ED\u30B8\u30AB\u30EB",
      // Russian
      "\u043A\u0432\u0430\u043D\u0442\u043E\u0432\u044B\u0439",
      "\u0444\u043E\u0442\u043E\u043D\u0438\u043A\u0430",
      "\u0433\u0435\u043D\u043E\u043C\u0438\u043A\u0430",
      "\u043F\u0440\u043E\u0442\u0435\u043E\u043C\u0438\u043A\u0430",
      "\u0442\u043E\u043F\u043E\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0439",
      "\u0433\u043E\u043C\u043E\u043C\u043E\u0440\u0444\u043D\u044B\u0439",
      "\u0441 \u043D\u0443\u043B\u0435\u0432\u044B\u043C \u0440\u0430\u0437\u0433\u043B\u0430\u0448\u0435\u043D\u0438\u0435\u043C",
      "\u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0440\u0435\u0448\u0451\u0442\u043E\u043A",
      // German
      "quanten",
      "photonik",
      "genomik",
      "proteomik",
      "topologisch",
      "homomorph",
      "zero-knowledge",
      "gitterbasiert",
      // Spanish
      "cu\xE1ntico",
      "fot\xF3nica",
      "gen\xF3mica",
      "prote\xF3mica",
      "topol\xF3gico",
      "homom\xF3rfico",
      // Portuguese
      "qu\xE2ntico",
      "fot\xF4nica",
      "gen\xF4mica",
      "prote\xF4mica",
      "topol\xF3gico",
      "homom\xF3rfico",
      // Korean
      "\uC591\uC790",
      "\uD3EC\uD1A0\uB2C9\uC2A4",
      "\uC720\uC804\uCCB4\uD559",
      "\uC704\uC0C1",
      "\uB3D9\uD615",
      // Arabic
      "\u0643\u0645\u064A",
      "\u0636\u0648\u0626\u064A\u0627\u062A",
      "\u062C\u064A\u0646\u0648\u0645\u064A\u0627\u062A",
      "\u0637\u0648\u0628\u0648\u0644\u0648\u062C\u064A",
      "\u062A\u0645\u0627\u062B\u0644\u064A"
    ],
    // Agentic task keywords - file ops, execution, multi-step, iterative work
    // Pruned: removed overly common words like "then", "first", "run", "test", "build"
    agenticTaskKeywords: [
      // English - File operations (clearly agentic)
      "read file",
      "read the file",
      "look at",
      "check the",
      "open the",
      "edit",
      "modify",
      "update the",
      "change the",
      "write to",
      "create file",
      // English - Execution (specific commands only)
      "execute",
      "deploy",
      "install",
      "npm",
      "pip",
      "compile",
      // English - Multi-step patterns (specific only)
      "after that",
      "and also",
      "once done",
      "step 1",
      "step 2",
      // English - Iterative work
      "fix",
      "debug",
      "until it works",
      "keep trying",
      "iterate",
      "make sure",
      "verify",
      "confirm",
      // Chinese (keep specific ones)
      "\u8BFB\u53D6\u6587\u4EF6",
      "\u67E5\u770B",
      "\u6253\u5F00",
      "\u7F16\u8F91",
      "\u4FEE\u6539",
      "\u66F4\u65B0",
      "\u521B\u5EFA",
      "\u6267\u884C",
      "\u90E8\u7F72",
      "\u5B89\u88C5",
      "\u7B2C\u4E00\u6B65",
      "\u7B2C\u4E8C\u6B65",
      "\u4FEE\u590D",
      "\u8C03\u8BD5",
      "\u76F4\u5230",
      "\u786E\u8BA4",
      "\u9A8C\u8BC1",
      // Spanish
      "leer archivo",
      "editar",
      "modificar",
      "actualizar",
      "ejecutar",
      "desplegar",
      "instalar",
      "paso 1",
      "paso 2",
      "arreglar",
      "depurar",
      "verificar",
      // Portuguese
      "ler arquivo",
      "editar",
      "modificar",
      "atualizar",
      "executar",
      "implantar",
      "instalar",
      "passo 1",
      "passo 2",
      "corrigir",
      "depurar",
      "verificar",
      // Korean
      "\uD30C\uC77C \uC77D\uAE30",
      "\uD3B8\uC9D1",
      "\uC218\uC815",
      "\uC5C5\uB370\uC774\uD2B8",
      "\uC2E4\uD589",
      "\uBC30\uD3EC",
      "\uC124\uCE58",
      "\uB2E8\uACC4 1",
      "\uB2E8\uACC4 2",
      "\uB514\uBC84\uADF8",
      "\uD655\uC778",
      // Arabic
      "\u0642\u0631\u0627\u0621\u0629 \u0645\u0644\u0641",
      "\u062A\u062D\u0631\u064A\u0631",
      "\u062A\u0639\u062F\u064A\u0644",
      "\u062A\u062D\u062F\u064A\u062B",
      "\u062A\u0646\u0641\u064A\u0630",
      "\u0646\u0634\u0631",
      "\u062A\u062B\u0628\u064A\u062A",
      "\u0627\u0644\u062E\u0637\u0648\u0629 1",
      "\u0627\u0644\u062E\u0637\u0648\u0629 2",
      "\u0625\u0635\u0644\u0627\u062D",
      "\u062A\u0635\u062D\u064A\u062D",
      "\u062A\u062D\u0642\u0642"
    ],
    // Dimension weights (sum to 1.0)
    dimensionWeights: {
      tokenCount: 0.08,
      codePresence: 0.15,
      reasoningMarkers: 0.18,
      technicalTerms: 0.1,
      creativeMarkers: 0.05,
      simpleIndicators: 0.02,
      // Reduced from 0.12 to make room for agenticTask
      multiStepPatterns: 0.12,
      questionComplexity: 0.05,
      imperativeVerbs: 0.03,
      constraintCount: 0.04,
      outputFormat: 0.03,
      referenceComplexity: 0.02,
      negationComplexity: 0.01,
      domainSpecificity: 0.02,
      agenticTask: 0.04
      // Reduced - agentic signals influence tier selection, not dominate it
    },
    // Tier boundaries on weighted score axis
    tierBoundaries: {
      simpleMedium: 0,
      mediumComplex: 0.3,
      // Raised from 0.18 - prevent simple tasks from reaching expensive COMPLEX tier
      complexReasoning: 0.5
      // Raised from 0.4 - reserve for true reasoning tasks
    },
    // Sigmoid steepness for confidence calibration
    confidenceSteepness: 12,
    // Below this confidence → ambiguous (null tier)
    confidenceThreshold: 0.7
  },
  // ─── Tier chains ───
  //
  // Catalog refresh 2026-08-29 (V3.5). Every chain below names only models
  // the public catalog lists (GET https://blockrun.ai/api/v1/models). Ids the
  // gateway withholds (`hidden: true`) — kimi-k2.5/k2.6/k2.7, the grok-4-fast
  // and grok-4-1-fast pairs, grok-4-0709, claude-opus-4.6, gemini-3-pro-preview,
  // the whole `free/*` namespace — were removed everywhere, including fallback
  // rungs, so a routed model is always one a user can find on blockrun.ai/models.
  //
  // Primaries moved only where portfolio.ts already carries calibration
  // evidence for the successor (Sonnet 5 over Sonnet 4.6, GPT-5 Mini for
  // agentic MEDIUM, Gemini 3.5 Flash where Kimi K2.7 was). Newcomers with no
  // trajectory evidence yet (gemini-3.6-flash, glm-5.3, glm-5.3-flash,
  // gpt-5.6-luna, grok-4.3, minimax-m3, qwen3.7-plus) enter as fallback rungs;
  // promotion waits for a calibration run, because version recency is not a
  // quality signal.
  //
  // Latency figures in comments are the 2026-08-29 gateway probe
  // (model-profiles.generated.json); prices are the catalog list.
  // Auto (balanced) tier configs - current default smart routing
  tiers: {
    SIMPLE: {
      primary: "google/gemini-2.5-flash",
      // $0.30/$2.50 — 60% retention (best) in the 2026-03 run; still the fastest quality answer
      fallback: [
        "google/gemini-3-flash-preview",
        // $0.50/$3 — GPQA 5/6 in the 2026-07 calibration
        "google/gemini-3.5-flash-lite",
        // $0.30/$2.50, 1M ctx, thinking mode — same price as 2.5 Flash, newer generation
        "deepseek/deepseek-chat",
        // $0.14/$0.28, 1M ctx
        "google/gemini-3.1-flash-lite",
        // $0.25/$1.50, 1M ctx
        "openai/gpt-5.6-luna",
        // $0.20/$1.20, 1M ctx — GPT-5.6 cost tier (cut 2026-07-30)
        "openai/gpt-5.4-nano",
        // $0.20/$1.25, 1M ctx
        "google/gemini-2.5-flash-lite",
        // $0.10/$0.40
        "nvidia/nemotron-3.5-lightning"
        // FREE backstop — NVIDIA free tier (probed 2026-08-30)
      ]
    },
    MEDIUM: {
      // Was moonshot/kimi-k2.7 (hidden 2026-08). Gemini 3.5 Flash is the
      // calibrated successor: MGSM 5/5, GPQA 4/6, extraction band (portfolio.ts).
      primary: "google/gemini-3.5-flash",
      // $1.50/$9, 1M ctx, vision + tools
      fallback: [
        "google/gemini-3.6-flash",
        // $1.50/$7.50 — newest Flash, output 17% cheaper than 3.5; awaiting calibration
        "zai/glm-5.3-flash",
        // $0.15/$0.50, 1M ctx, vision + tools verified live 2026-08-27
        "openai/gpt-5.6-terra",
        // $2/$12, 1M ctx — GPT-5.6 balanced tier
        "google/gemini-3-flash-preview",
        // $0.50/$3
        "deepseek/deepseek-chat",
        // $0.14/$0.28
        "google/gemini-2.5-flash",
        // $0.30/$2.50
        "minimax/minimax-m3",
        // $0.30/$1.20, 1M ctx
        "google/gemini-3.1-flash-lite",
        // $0.25/$1.50
        "openai/gpt-5.6-luna",
        // $0.20/$1.20
        "google/gemini-2.5-flash-lite"
        // $0.10/$0.40
      ]
    },
    COMPLEX: {
      primary: "google/gemini-3.1-pro",
      // $2/$12 — proven long-context flagship (portfolio.ts long_context lead)
      fallback: [
        "google/gemini-3.6-flash",
        // $1.50/$7.50 — Pro-level quality at Flash price (Google's claim; uncalibrated here)
        "google/gemini-3.5-flash",
        // $1.50/$9 — calibrated
        "anthropic/claude-sonnet-5",
        // $3/$15 — near-Opus quality, tau2 + Terminal-Bench calibrated
        "xai/grok-4.5",
        // $2.50/$9 — 503-resistant, independent infra (was grok-4-0709, now hidden)
        "google/gemini-2.5-pro",
        // $1.25/$10
        "anthropic/claude-sonnet-4.6",
        // $3/$15
        "openai/gpt-5.6-terra",
        // $2/$12 — GPT-5.6 balanced tier (Sol excluded: #202)
        "openai/gpt-5.5",
        // $5/$30 — prior OpenAI flagship
        "openai/gpt-5.4",
        // $2.50/$15 — previous flagship, benchmarked
        "zai/glm-5.3",
        // $1.40/$4.40, 1M ctx, always-on thinking — verified live 2026-08-19
        "moonshot/kimi-k3",
        // $3/$15, 1M ctx — Moonshot flagship (K2.7 successor)
        "deepseek/deepseek-v4-pro",
        // $0.435/$0.87 — strongest open-weight reasoner
        "deepseek/deepseek-chat",
        // $0.14/$0.28 — cheap last resort
        "google/gemini-2.5-flash"
        // $0.30/$2.50
      ]
    },
    REASONING: {
      // Was xai/grok-4-1-fast-reasoning ($0.20/$0.50, hidden 2026-08). DeepSeek
      // Reasoner is the cheapest listed reasoner at the same 1M context.
      primary: "deepseek/deepseek-reasoner",
      // $0.14/$0.28, 1M ctx
      fallback: [
        "deepseek/deepseek-v4-pro",
        // $0.435/$0.87 — calibrated reasoning band 0.95
        "xai/grok-4.3",
        // $1.50/$4, 1M ctx — xAI reasoning model, vision
        "qwen/qwen3.7-plus",
        // $0.32/$1.28, 1M ctx — reasoning; needs a generous max_tokens (thinking is billed)
        "google/gemini-3.5-flash",
        // $1.50/$9 — MGSM 5/5
        "openai/o4-mini",
        // $1.10/$4.40
        "openai/o3"
        // $2/$8
      ]
    }
  },
  // Eco tier configs - absolute cheapest (blockrun/eco)
  ecoTiers: {
    SIMPLE: {
      primary: "nvidia/nemotron-3.5-lightning",
      // FREE — NVIDIA free tier flagship, 1M ctx
      fallback: [
        // Was nvidia/nemotron-3-nano-30b until blockrun delisted it on 2026-09-08
        // (NVIDIA deprovisioned it for the account — a structured per-account
        // 404). nano-omni is blockrun's own redirect target for that id, so this
        // follows the same rule as the head: the router and the gateway name the
        // same model.
        "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning",
        // FREE — nano-30b's successor, 256K ctx
        // The free head keeps rotting with NVIDIA's hosting (deepseek-v4-flash
        // 410 2026-08-12, seed-oss-36b 410 2026-08-03, gpt-oss-120b/20b 400
        // 2026-08-21, and on 2026-08-30 FOUR of the five visible free models at
        // once — step-3.7-flash, nemotron-nano-9b-v2 and nemotron-nano-12b-v2-vl
        // all 410, mistral-nemotron hung). Each retirement retargets the two
        // free rungs to the current free tier; the paid rungs below never move.
        // The head follows blockrun's own redirect of the model it replaces, so
        // the router and the gateway never name different models.
        "google/gemini-2.5-flash-lite",
        // $0.10/$0.40 — cheapest paid rung
        "zai/glm-5.3-flash",
        // $0.15/$0.50, 1M ctx, vision + tools
        "openai/gpt-5.6-luna",
        // $0.20/$1.20, 1M ctx
        "openai/gpt-5.4-nano",
        // $0.20/$1.25
        "google/gemini-3.1-flash-lite"
        // $0.25/$1.50
      ]
    },
    MEDIUM: {
      primary: "zai/glm-5.3-flash",
      // $0.15/$0.50, 1M ctx, vision + tools verified live — cheapest full-capability model
      fallback: [
        "deepseek/deepseek-chat",
        // $0.14/$0.28
        "google/gemini-3.1-flash-lite",
        // $0.25/$1.50
        "openai/gpt-5.6-luna",
        // $0.20/$1.20
        "openai/gpt-5.4-nano",
        // $0.20/$1.25
        "google/gemini-2.5-flash-lite",
        // $0.10/$0.40
        "google/gemini-2.5-flash"
        // $0.30/$2.50
      ]
    },
    COMPLEX: {
      primary: "zai/glm-5.3-flash",
      // $0.15/$0.50, 1M ctx
      fallback: [
        "deepseek/deepseek-chat",
        // $0.14/$0.28, 1M ctx
        "minimax/minimax-m3",
        // $0.30/$1.20, 1M ctx
        "deepseek/deepseek-v4-pro",
        // $0.435/$0.87
        "google/gemini-3.1-flash-lite",
        // $0.25/$1.50
        "google/gemini-2.5-flash"
        // $0.30/$2.50
      ]
    },
    REASONING: {
      primary: "deepseek/deepseek-reasoner",
      // $0.14/$0.28, 1M ctx — cheapest listed reasoner
      fallback: [
        "deepseek/deepseek-v4-pro",
        // $0.435/$0.87
        "qwen/qwen3.7-plus",
        // $0.32/$1.28 — reasoning
        "minimax/minimax-m3",
        // $0.30/$1.20 — reasoning + coding
        "zai/glm-5.3-flash"
        // $0.15/$0.50 — reasoning tokens alongside content
      ]
    }
  },
  // Premium tier configs - best quality (blockrun/premium)
  // codex=complex coding, flash=simple coding, sonnet=reasoning/instructions, fable/opus=architecture/PM/audits
  premiumTiers: {
    SIMPLE: {
      // Was moonshot/kimi-k2.7 (hidden 2026-08).
      primary: "google/gemini-3.5-flash",
      // $1.50/$9, 1M ctx, vision + tools — calibrated
      fallback: [
        "google/gemini-3.6-flash",
        // $1.50/$7.50 — newest Flash
        "anthropic/claude-haiku-4.5",
        // $1/$5
        "zai/glm-5.3",
        // $1.40/$4.40, 1M ctx
        "google/gemini-2.5-flash",
        // $0.30/$2.50
        "google/gemini-3.5-flash-lite",
        // $0.30/$2.50
        "deepseek/deepseek-chat"
        // $0.14/$0.28
      ]
    },
    MEDIUM: {
      primary: "openai/gpt-5.3-codex",
      // $1.75/$14 - 400K context, 128K output — code_edit/debug lead (portfolio.ts)
      fallback: [
        "anthropic/claude-sonnet-5",
        // $3/$15 — code_agent band 0.98
        "moonshot/kimi-k3",
        // $3/$15, 1M ctx — Moonshot flagship
        "zai/glm-5.3",
        // $1.40/$4.40 — long-horizon coding
        "google/gemini-3.6-flash",
        // $1.50/$7.50
        "google/gemini-3.5-flash",
        // $1.50/$9
        "google/gemini-2.5-pro",
        // $1.25/$10
        "xai/grok-4.5",
        // $2.50/$9
        "anthropic/claude-sonnet-4.6",
        // $3/$15
        "openai/gpt-5.6-terra"
        // $2/$12
      ]
    },
    COMPLEX: {
      // fable-5 was promoted here 2026-06-11, force-reverted 2026-06-13 when Anthropic
      // withdrew the offer, and restored 2026-07-14 now that BlockRun has relisted it.
      primary: "anthropic/claude-fable-5",
      // Best quality for complex tasks — Mythos-class flagship above Opus ($10/$50, 1M ctx, always-on thinking)
      // Fallback chain de-Gemini'd 2026-04-22: when Anthropic 503s, Gemini is
      // also prone to "high demand" 503s (correlated failure — everyone falls
      // back to Google at the same time). Prefer in-family → xAI → Moonshot →
      // OpenAI flagship → Z.AI → DeepSeek → NVIDIA free instead.
      fallback: [
        "anthropic/claude-opus-5",
        // in-family hot swap first (half the price, 1M ctx + adaptive thinking)
        "anthropic/claude-opus-4.8",
        // in-family hot swap (identical cost to 5)
        "anthropic/claude-opus-4.7",
        // in-family hot swap (identical cost to 4.8)
        "anthropic/claude-sonnet-5",
        // Sonnet-tier drop-down, near-Opus quality
        "anthropic/claude-sonnet-4.6",
        "xai/grok-4.5",
        // xAI flagship — 503-resistant, direct-xAI SKU
        "moonshot/kimi-k3",
        // Moonshot flagship, independent infra
        "openai/gpt-5.6-terra",
        // GPT-5.6 balanced tier — stable (Sol excluded: #202)
        "openai/gpt-5.5",
        // Prior OpenAI flagship — 1M+ ctx, native agent + computer use
        "openai/gpt-5.4",
        // Previous flagship (slow but stable, benchmarked at 6,213ms)
        "openai/gpt-5.3-codex",
        "zai/glm-5.3",
        // Z.AI flagship, 1M ctx
        "deepseek/deepseek-v4-pro",
        // strongest open-weight reasoner
        "deepseek/deepseek-chat",
        // Cheap, reliable
        "nvidia/nemotron-3.5-lightning"
        // NVIDIA free ultimate backstop
      ]
    },
    REASONING: {
      // Sonnet 5 promoted over Sonnet 4.6 (same price; reasoning band 0.98 for both,
      // plus Sonnet 5's tau2/BrowseComp trajectory evidence).
      primary: "anthropic/claude-sonnet-5",
      // $3/$15, 1M ctx, adaptive thinking
      fallback: [
        "anthropic/claude-sonnet-4.6",
        // in-family hot swap — same cost
        "anthropic/claude-opus-5",
        // Newest flagship Opus w/ adaptive thinking
        "anthropic/claude-opus-4.8",
        // Prior flagship Opus — identical cost to 5
        "anthropic/claude-opus-4.7",
        // Flagship Opus w/ adaptive thinking
        "xai/grok-4.5",
        // reasoning band 0.94
        "deepseek/deepseek-v4-pro",
        // reasoning band 0.95
        "xai/grok-4.3",
        // $1.50/$4 — xAI reasoning model
        "openai/o4-mini",
        // $1.10/$4.40
        "openai/o3"
        // $2/$8
      ]
    }
  },
  // Agentic tier configs - models that excel at multi-step autonomous tasks
  agenticTiers: {
    SIMPLE: {
      primary: "openai/gpt-4o-mini",
      // $0.15/$0.60 - best tool compliance at lowest cost
      fallback: [
        "openai/gpt-5.6-luna",
        // $0.20/$1.20 — lightweight agentic tier of GPT-5.6
        "zai/glm-5.3-flash",
        // $0.15/$0.50 — tool calls verified live 2026-08-27
        "anthropic/claude-haiku-4.5",
        // $1/$5
        "google/gemini-2.5-flash"
        // $0.30/$2.50
      ]
    },
    MEDIUM: {
      // Was moonshot/kimi-k2.7 (hidden 2026-08). GPT-5 Mini carries the
      // Terminal-Bench and tau2 trajectory evidence in portfolio.ts.
      primary: "openai/gpt-5-mini",
      // $0.25/$2 — 4/7 Terminal-Bench, 5/6 tau2 airline
      fallback: [
        "google/gemini-3.5-flash",
        // $1.50/$9 — tool_agent band 0.88
        "zai/glm-5.3-flash",
        // $0.15/$0.50 — tools verified
        "openai/gpt-5.6-terra",
        // $2/$12
        "openai/gpt-4o-mini",
        // $0.15/$0.60 — reliable tool calling
        "anthropic/claude-haiku-4.5",
        // $1/$5
        "deepseek/deepseek-chat",
        // $0.14/$0.28
        "moonshot/kimi-k3"
        // $3/$15 — tool_agent band 0.85
      ]
    },
    COMPLEX: {
      // Sonnet 5 promoted over Sonnet 4.6: tau2 airline + retail reward 1.0,
      // Terminal-Bench safety band lead (portfolio.ts).
      primary: "anthropic/claude-sonnet-5",
      // $3/$15 — best agentic quality per trajectory evidence
      // Fallback chain de-Gemini'd 2026-04-22: Gemini's "high demand" 503s
      // correlate with Anthropic outages (everyone falls back together).
      // Prefer 503-resistant providers first.
      fallback: [
        "anthropic/claude-sonnet-4.6",
        // in-family hot swap — same cost
        "anthropic/claude-opus-5",
        // Newest flagship Opus — in-family hot swap
        "anthropic/claude-opus-4.8",
        // Prior flagship Opus — identical cost to 5
        "anthropic/claude-opus-4.7",
        // Flagship Opus — in-family hot swap
        "xai/grok-4.5",
        // xAI flagship — strong tool use, independent infra
        "moonshot/kimi-k3",
        // Moonshot flagship — independent infra
        "openai/gpt-5.6-terra",
        // GPT-5.6 balanced tier — stable (Sol excluded: #202)
        "openai/gpt-5.5",
        // Prior flagship — native agent + computer use (exactly the agentic-tier use case)
        "openai/gpt-5.4",
        // Previous flagship — reliable
        "openai/gpt-5.3-codex",
        // code_agent lead
        "zai/glm-5.3",
        // long-horizon coding
        "deepseek/deepseek-v4-pro",
        // retail high-risk 3/3
        "deepseek/deepseek-chat",
        // cheap, reliable
        "nvidia/nemotron-3.5-lightning"
        // NVIDIA free ultimate backstop
      ]
    },
    REASONING: {
      primary: "anthropic/claude-sonnet-5",
      // $3/$15 — strong tool use + adaptive thinking
      fallback: [
        "anthropic/claude-sonnet-4.6",
        // in-family hot swap — same cost
        "anthropic/claude-opus-5",
        // Newest flagship Opus w/ adaptive thinking
        "anthropic/claude-opus-4.8",
        // Prior flagship Opus — identical cost to 5
        "anthropic/claude-opus-4.7",
        // Flagship Opus w/ adaptive thinking
        "xai/grok-4.5",
        // reasoning band 0.94
        "deepseek/deepseek-v4-pro",
        // reasoning band 0.95
        "deepseek/deepseek-reasoner"
        // $0.14/$0.28
      ]
    }
  },
  // Time-windowed promotions — auto-applied when active, ignored when expired.
  // The GLM-5.1 launch promo (2026-04-01 → 2026-05-01) was the last entry and
  // has expired; the list is kept empty so the mechanism stays wired.
  promotions: [],
  overrides: {
    maxTokensForceComplex: 1e5,
    structuredOutputMinTier: "MEDIUM",
    ambiguousDefaultTier: "MEDIUM"
    // agenticMode left undefined → auto-detect via tools/agenticScore.
    // Set to `true` to force agentic tiers; `false` to disable them entirely.
  }
};
registerStrategy(new PortfolioStrategy());
function route(prompt, systemPrompt, maxOutputTokens, options) {
  const strategy = getStrategy(options.config.strategy ?? "portfolio");
  return strategy.route(prompt, systemPrompt, maxOutputTokens, options);
}

// src/router-adapter.ts
var AUTO_ROUTING_PROFILES = {
  "blockrun/auto": "auto",
  "blockrun/eco": "eco",
  "blockrun/premium": "premium"
};
var BASE_MINIMUM_PAYMENT_USD = 2e-3;
var SOLANA_MINIMUM_PAYMENT_USD = 1e-3;
function isTransientError(err) {
  if (retryDisposition(err) !== "unpaid") return false;
  if (err instanceof PaymentError) return false;
  if (err instanceof APIError) {
    return [429, 502, 503, 504, 522, 524].includes(err.statusCode);
  }
  if (err instanceof Error) {
    if (err.name === "AbortError") return true;
    if (err.name === "TypeError" && /fetch|network/i.test(err.message)) return true;
  }
  return false;
}
function errSummary(err) {
  if (err instanceof APIError) return `APIError ${err.statusCode}`;
  if (err instanceof Error) {
    const msg = err.message.length > 80 ? err.message.slice(0, 80) : err.message;
    return `${err.name}: ${msg}`;
  }
  return String(err).slice(0, 100);
}
function requestsSearch(options) {
  if (options.tools?.some((tool) => tool.type === "x_search" || tool.type === "web_search")) return true;
  if (options.searchParameters !== void 0) return options.searchParameters.mode !== "off";
  return options.search === true;
}
var searchModelsByCatalog = /* @__PURE__ */ new WeakMap();
function registerSearchModels(pricing, models) {
  const xaiChat = models.filter(
    (m) => m.id.startsWith("xai/") && m.available !== false && Array.isArray(m.categories) && m.categories.includes("chat") && pricing.has(m.id)
  );
  const tagged = xaiChat.filter((m) => m.categories.includes("search"));
  searchModelsByCatalog.set(pricing, (tagged.length > 0 ? tagged : xaiChat).map((m) => m.id));
}
function searchFallbackChain(modelPricing, routingProfile) {
  const pool = searchModelsByCatalog.get(modelPricing) ?? [...modelPricing.keys()].filter((id) => id.startsWith("xai/"));
  const price = (id) => {
    const p = modelPricing.get(id);
    return p ? p.inputPrice + p.outputPrice : Number.POSITIVE_INFINITY;
  };
  const sorted = [...pool].sort((a, b) => price(a) - price(b) || b.localeCompare(a, void 0, { numeric: true }));
  if (routingProfile !== "premium") return sorted;
  return [...pool].sort((a, b) => price(b) - price(a) || b.localeCompare(a, void 0, { numeric: true }));
}
function routingProfileForModel(model) {
  return AUTO_ROUTING_PROFILES[model.toLowerCase()];
}
function routingText(messages) {
  const systemPrompt = messages.filter((message) => message.role === "system" && typeof message.content === "string").map((message) => message.content).join("\n") || void 0;
  const lastUser = [...messages].reverse().find((message) => message.role === "user" && typeof message.content === "string");
  const lastText = [...messages].reverse().find((message) => typeof message.content === "string");
  let conversationChars = 0;
  let hasVision = false;
  for (const message of messages) {
    if (typeof message.content === "string") {
      conversationChars += message.content.length;
    } else if (Array.isArray(message.content)) {
      for (const part of message.content) {
        if (part?.type === "image_url" || part?.type === "image") hasVision = true;
        if (typeof part?.text === "string") conversationChars += part.text.length;
      }
    }
  }
  return {
    prompt: lastUser?.content ?? lastText?.content ?? "",
    systemPrompt,
    conversationChars,
    hasVision
  };
}
function routeWithCatalog(prompt, systemPrompt, maxOutputTokens, modelPricing, options = {}) {
  const tools = (options.tools ?? []).filter((tool) => tool.type === "function");
  const requiresTools = options.toolChoice === "none" ? false : options.toolChoice === "required" || typeof options.toolChoice === "object" ? true : void 0;
  const decision = route(prompt, systemPrompt, maxOutputTokens, {
    config: DEFAULT_ROUTING_CONFIG,
    modelPricing,
    routingProfile: options.routingProfile,
    hasTools: tools.length > 0,
    toolCount: tools.length,
    toolNames: tools.map((tool) => tool.function.name),
    requiresTools,
    hasVision: options.hasVision,
    requiresStructuredOutput: options.requiresStructuredOutput
  });
  const tierConfigs = decision.tierConfigs ?? DEFAULT_ROUTING_CONFIG.tiers;
  const ranked = decision.candidates?.length ? decision.candidates : [decision.model, ...getFallbackChain(decision.tier, tierConfigs)];
  const callable = [];
  for (const id of ranked) {
    const resolved = !id.startsWith("free/") ? id : modelPricing.has(`nvidia/${id.slice(5)}`) ? `nvidia/${id.slice(5)}` : null;
    if (resolved && !callable.includes(resolved)) callable.push(resolved);
  }
  let searchFallback = false;
  if (requestsSearch(options)) {
    let searchable = callable.filter((id) => id.startsWith("xai/"));
    if (searchable.length === 0) {
      searchable = searchFallbackChain(modelPricing, options.routingProfile);
      searchFallback = true;
    }
    if (searchable.length === 0) {
      throw new Error(
        `Search (x_search / web_search tools or searchParameters) runs on xai/* models only, and the catalog lists no available xai/* chat model. Call an xai/* model directly or drop the search options.`
      );
    }
    callable.splice(0, callable.length, ...searchable);
  }
  const estimatedInputTokens = Math.ceil(
    Math.max(options.conversationChars ?? 0, `${systemPrompt ?? ""} ${prompt}`.length) / 4
  );
  const fitting = filterCandidatesByCapacity(
    callable,
    estimatedInputTokens,
    maxOutputTokens,
    (id) => {
      const caps = DEFAULT_MODEL_CAPABILITIES[id];
      return caps ? { contextWindow: caps.contextWindow, maxOutput: caps.maxOutputTokens } : void 0;
    }
  );
  const availableCandidates = fitting.length > 0 ? fitting : callable;
  const model = availableCandidates[0] ?? decision.model;
  const costs = calculateModelCost(
    model,
    modelPricing,
    estimatedInputTokens,
    maxOutputTokens,
    options.routingProfile
  );
  const minimumPaymentUsd = options.minimumPaymentUsd ?? SOLANA_MINIMUM_PAYMENT_USD;
  const entry = modelPricing.get(model);
  const isFree = entry !== void 0 && entry.inputPrice === 0 && entry.outputPrice === 0 && !entry.flatPrice;
  const costEstimate = isFree ? 0 : Math.max(costs.costEstimate, minimumPaymentUsd);
  const savings = options.routingProfile === "premium" || costs.baselineCost <= 0 ? 0 : entry !== void 0 ? Math.max(0, (costs.baselineCost - costEstimate) / costs.baselineCost) : decision.savings;
  return {
    ...decision,
    ...costs,
    costEstimate,
    savings,
    model,
    reasoning: searchFallback ? `${decision.reasoning} | search runs on xai/* only; the route had none, so the ${options.routingProfile === "premium" ? "most capable" : "cheapest"} search-capable xAI model: ${model}` : model === decision.model ? decision.reasoning : `${decision.reasoning} | catalog fallback: ${model}`,
    candidates: availableCandidates,
    candidateScores: decision.candidateScores?.filter(
      (score) => availableCandidates.includes(score.model)
    ),
    fallbacks: availableCandidates.slice(1)
  };
}

// src/x402.ts
var import_accounts = require("viem/accounts");

// src/solana-deps.ts
var INSTALL_HINT = "npm install @solana/web3.js @solana/spl-token   (or pnpm add / yarn add)";
function missing(pkg, what, cause) {
  return new Error(
    `@blockrun/llm: ${what} requires the optional peer dependency "${pkg}", which is not installed.

  ${INSTALL_HINT}

Solana packages are optional peers so that consumers who only use Base/EVM payments do not inherit them. If you only make Base payments, you should not be reaching this code path \u2014 check which chain you passed.
Original error: ${cause instanceof Error ? cause.message : String(cause)}`
  );
}
async function loadSolanaWeb3(what) {
  try {
    return await import("@solana/web3.js");
  } catch (err) {
    throw missing("@solana/web3.js", what, err);
  }
}
async function loadSplToken(what) {
  try {
    return await import("@solana/spl-token");
  } catch (err) {
    throw missing("@solana/spl-token", what, err);
  }
}

// src/x402.ts
var BASE_CHAIN_ID = 8453;
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
var ARC_CHAIN_ID = 5042;
var USDC_ARC = "0x3600000000000000000000000000000000000000";
var EVM_NETWORKS = {
  "eip155:8453": {
    name: "Base",
    chainId: BASE_CHAIN_ID,
    usdc: USDC_BASE,
    domain: { name: "USD Coin", version: "2", chainId: BASE_CHAIN_ID, verifyingContract: USDC_BASE }
  },
  "eip155:5042": {
    name: "Arc",
    chainId: ARC_CHAIN_ID,
    usdc: USDC_ARC,
    domain: { name: "USDC", version: "2", chainId: ARC_CHAIN_ID, verifyingContract: USDC_ARC }
  },
  "eip155:84532": {
    name: "Base Sepolia",
    chainId: 84532,
    usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    domain: {
      name: "USDC",
      version: "2",
      chainId: 84532,
      verifyingContract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
    }
  }
};
function evmNetwork(network) {
  const net = EVM_NETWORKS[network];
  if (!net) {
    throw new Error(
      `Unsupported x402 network "${network}": this SDK signs USDC payments on ${Object.keys(EVM_NETWORKS).join(", ")}`
    );
  }
  return net;
}
var SOLANA_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
var USDC_SOLANA = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
var SOLANA_USDC_DECIMALS = 6;
var DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1;
var DEFAULT_COMPUTE_UNIT_LIMIT = 8e3;
var BLOCKHASH_TTL_MS = 1e4;
var MAX_FEE_NONCE_STEPS = 64;
var MAX_TRACKED_BLOCKHASHES = 8;
var MAX_TRACKED_ENDPOINTS = 8;
var blockhashCache = /* @__PURE__ */ new Map();
function trimOldest(map, limit) {
  while (map.size > limit) {
    const oldest = map.keys().next().value;
    map.delete(oldest);
  }
}
var issuedByBlockhash = /* @__PURE__ */ new Map();
function issuedFor(blockhash) {
  let issued = issuedByBlockhash.get(blockhash);
  if (!issued) {
    issued = /* @__PURE__ */ new Set();
    issuedByBlockhash.set(blockhash, issued);
    trimOldest(issuedByBlockhash, MAX_TRACKED_BLOCKHASHES);
  }
  return issued;
}
async function getBlockhashEntry(connection, rpcUrl, forceRefresh) {
  const now = Date.now();
  const cached = blockhashCache.get(rpcUrl);
  if (!forceRefresh && cached && now - cached.fetchedAt < BLOCKHASH_TTL_MS) {
    return cached;
  }
  const { blockhash } = await connection.getLatestBlockhash();
  const current = blockhashCache.get(rpcUrl);
  if (current?.blockhash === blockhash) {
    current.fetchedAt = now;
    return current;
  }
  const entry = { blockhash, fetchedAt: now };
  blockhashCache.set(rpcUrl, entry);
  trimOldest(blockhashCache, MAX_TRACKED_ENDPOINTS);
  return entry;
}
var TRANSFER_TYPES = {
  TransferWithAuthorization: [
    { name: "from", type: "address" },
    { name: "to", type: "address" },
    { name: "value", type: "uint256" },
    { name: "validAfter", type: "uint256" },
    { name: "validBefore", type: "uint256" },
    { name: "nonce", type: "bytes32" }
  ]
};
function createNonce() {
  const bytes = new Uint8Array(32);
  crypto.getRandomValues(bytes);
  return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
}
var BLOCKRUN_SERVICE_CODE = "blockrun";
function withBuilderCodeServiceCode(extensions) {
  const merged = { ...extensions || {} };
  const existing = merged["builder-code"] || {};
  merged["builder-code"] = {
    ...existing,
    info: { ...existing.info || {}, s: [BLOCKRUN_SERVICE_CODE] }
  };
  return merged;
}
async function createPaymentPayload(privateKey, fromAddress, recipient, amount, network = "eip155:8453", options = {}) {
  const now = Math.floor(Date.now() / 1e3);
  const validAfter = now - 600;
  const validBefore = now + (options.maxTimeoutSeconds || 300);
  const nonce = createNonce();
  const net = evmNetwork(network);
  if (options.asset && options.asset.toLowerCase() !== net.usdc.toLowerCase()) {
    throw new Error(
      `x402 asset mismatch: the 402 asks for ${options.asset} on ${network}, but this SDK only pays USDC there (${net.usdc})`
    );
  }
  const domain = net.domain;
  const signature = await (0, import_accounts.signTypedData)({
    privateKey,
    domain,
    types: TRANSFER_TYPES,
    primaryType: "TransferWithAuthorization",
    message: {
      from: fromAddress,
      to: recipient,
      value: BigInt(amount),
      validAfter: BigInt(validAfter),
      validBefore: BigInt(validBefore),
      nonce
    }
  });
  const paymentData = {
    x402Version: 2,
    resource: {
      url: options.resourceUrl || "https://blockrun.ai/api/v1/chat/completions",
      description: options.resourceDescription || "BlockRun AI API call",
      mimeType: "application/json"
    },
    accepted: {
      scheme: "exact",
      network,
      amount,
      asset: net.usdc,
      payTo: recipient,
      maxTimeoutSeconds: options.maxTimeoutSeconds || 300,
      extra: { name: net.domain.name, version: net.domain.version }
    },
    payload: {
      signature,
      authorization: {
        from: fromAddress,
        to: recipient,
        value: amount,
        validAfter: validAfter.toString(),
        validBefore: validBefore.toString(),
        nonce
      }
    },
    extensions: withBuilderCodeServiceCode(options.extensions)
  };
  return btoa(JSON.stringify(paymentData));
}
async function createSolanaPaymentPayload(secretKey, fromAddress, recipient, amount, feePayer, options = {}) {
  const {
    Connection,
    PublicKey,
    TransactionMessage,
    VersionedTransaction,
    ComputeBudgetProgram,
    Keypair
  } = await loadSolanaWeb3("createSolanaPaymentPayload()");
  const { getAssociatedTokenAddress, createTransferCheckedInstruction } = await loadSplToken(
    "createSolanaPaymentPayload()"
  );
  const rpcUrl = options.rpcUrl || "https://sol.blockrun.ai/api/v1/solana/rpc";
  const connection = options.rpcHeaders ? new Connection(rpcUrl, { httpHeaders: options.rpcHeaders }) : new Connection(rpcUrl);
  const keypair = Keypair.fromSecretKey(secretKey);
  const feePayerPubkey = new PublicKey(feePayer);
  const ownerPubkey = keypair.publicKey;
  const tokenMint = new PublicKey(USDC_SOLANA);
  const payToPubkey = new PublicKey(recipient);
  const sourceATA = await getAssociatedTokenAddress(tokenMint, ownerPubkey, false);
  const destinationATA = await getAssociatedTokenAddress(tokenMint, payToPubkey, false);
  const transferIx = createTransferCheckedInstruction(
    sourceATA,
    tokenMint,
    destinationATA,
    ownerPubkey,
    BigInt(amount),
    SOLANA_USDC_DECIMALS
  );
  const buildSignedTx = (blockhash, unitPriceMicroLamports) => {
    const messageV0 = new TransactionMessage({
      payerKey: feePayerPubkey,
      recentBlockhash: blockhash,
      instructions: [
        ComputeBudgetProgram.setComputeUnitLimit({ units: DEFAULT_COMPUTE_UNIT_LIMIT }),
        ComputeBudgetProgram.setComputeUnitPrice({ microLamports: unitPriceMicroLamports }),
        transferIx
      ]
    }).compileToV0Message();
    const transaction = new VersionedTransaction(messageV0);
    transaction.sign([keypair]);
    return Buffer.from(transaction.serialize()).toString("base64");
  };
  const findDistinctTx = (candidate) => {
    const issued = issuedFor(candidate.blockhash);
    for (let step = 0; step <= MAX_FEE_NONCE_STEPS; step++) {
      const tx = buildSignedTx(
        candidate.blockhash,
        DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS + step
      );
      if (!issued.has(tx)) return tx;
    }
    return null;
  };
  let entry = await getBlockhashEntry(connection, rpcUrl, options.forceFreshBlockhash ?? false);
  let serializedTx = findDistinctTx(entry);
  if (serializedTx === null) {
    entry = await getBlockhashEntry(connection, rpcUrl, true);
    serializedTx = findDistinctTx(entry);
  }
  if (serializedTx === null) {
    throw new Error(
      `Solana payment: ${MAX_FEE_NONCE_STEPS + 1} identical payments already issued against blockhash ${entry.blockhash}. Retry once it rotates.`
    );
  }
  issuedFor(entry.blockhash).add(serializedTx);
  const paymentData = {
    x402Version: 2,
    resource: {
      url: options.resourceUrl || "https://blockrun.ai/api/v1/chat/completions",
      description: options.resourceDescription || "BlockRun AI API call",
      mimeType: "application/json"
    },
    accepted: {
      scheme: "exact",
      network: SOLANA_NETWORK,
      amount,
      asset: USDC_SOLANA,
      payTo: recipient,
      maxTimeoutSeconds: options.maxTimeoutSeconds || 300,
      extra: options.extra || { feePayer }
    },
    payload: {
      transaction: serializedTx
    },
    extensions: withBuilderCodeServiceCode(options.extensions)
  };
  return btoa(JSON.stringify(paymentData));
}
function parsePaymentRequired(headerValue) {
  try {
    const decoded = atob(headerValue);
    const parsed = JSON.parse(decoded);
    if (!parsed.accepts || !Array.isArray(parsed.accepts)) {
      throw new Error("Invalid payment required structure: missing or invalid 'accepts' field");
    }
    return parsed;
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes("Invalid payment required structure")) {
        throw error;
      }
      throw new Error("Failed to parse payment required header: invalid format", {
        cause: error
      });
    }
    throw new Error("Failed to parse payment required header", { cause: error });
  }
}
function extractPaymentDetails(paymentRequired, preferredNetwork) {
  const accepts = paymentRequired.accepts || [];
  if (accepts.length === 0) {
    throw new Error("No payment options in payment required response");
  }
  let option = null;
  if (preferredNetwork) {
    option = accepts.find((opt) => opt.network === preferredNetwork) || null;
  }
  if (!option) {
    option = accepts[0];
  }
  const amount = option.amount || option.maxAmountRequired;
  if (!amount) {
    throw new Error("No amount found in payment requirements");
  }
  return {
    amount,
    recipient: option.payTo,
    network: option.network,
    asset: option.asset,
    scheme: option.scheme,
    maxTimeoutSeconds: option.maxTimeoutSeconds || 300,
    extra: option.extra,
    resource: paymentRequired.resource
  };
}

// src/cost-log.ts
var fs = __toESM(require("fs"), 1);
var path = __toESM(require("path"), 1);
var os = __toESM(require("os"), 1);
var BLOCKRUN_DIR = path.join(os.homedir(), ".blockrun");
var COST_LOG_FILE = path.join(BLOCKRUN_DIR, "cost_log.jsonl");
function logCost(entry) {
  try {
    fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
  } catch {
  }
  try {
    fs.appendFileSync(COST_LOG_FILE, JSON.stringify(entry) + "\n");
  } catch {
  }
}
function getCostSummary() {
  if (!fs.existsSync(COST_LOG_FILE)) {
    return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
  }
  let totalUsd = 0;
  let calls = 0;
  const byModel = {};
  const byEndpoint = {};
  try {
    const content = fs.readFileSync(COST_LOG_FILE, "utf-8").trim();
    if (!content) return { totalUsd: 0, calls: 0, byModel: {}, byEndpoint: {} };
    for (const line of content.split("\n")) {
      if (!line) continue;
      try {
        const raw = JSON.parse(line);
        const cost = typeof raw.cost_usd === "number" ? raw.cost_usd : raw.costUsd ?? 0;
        if (!cost) continue;
        totalUsd += cost;
        calls += 1;
        if (raw.model) byModel[raw.model] = (byModel[raw.model] || 0) + cost;
        if (raw.endpoint) byEndpoint[raw.endpoint] = (byEndpoint[raw.endpoint] || 0) + cost;
      } catch {
      }
    }
  } catch {
  }
  return { totalUsd, calls, byModel, byEndpoint };
}

// src/version.ts
var SDK_VERSION = "3.25.0";
var USER_AGENT = `blockrun-ts/${SDK_VERSION}`;

// src/client.ts
function mapRawToModel(m) {
  return {
    id: m.id,
    name: m.name || m.id,
    provider: m.provider || m.owned_by || "",
    description: m.description || "",
    inputPrice: m.inputPrice ?? m.input_price ?? m.pricing?.input ?? 0,
    outputPrice: m.outputPrice ?? m.output_price ?? m.pricing?.output ?? 0,
    contextWindow: m.contextWindow ?? m.context_window ?? 0,
    maxOutput: m.maxOutput ?? m.max_output ?? 0,
    categories: m.categories || [],
    available: true,
    billingMode: m.billingMode ?? m.billing_mode,
    flatPrice: m.flatPrice ?? m.flat_price ?? m.pricing?.flat,
    hidden: m.hidden
  };
}
function mapRawToImageModel(m) {
  return {
    id: m.id,
    name: m.name || m.id,
    provider: m.provider || m.owned_by || "",
    description: m.description || "",
    pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.per_image ?? m.pricing?.perImage ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
    supportedSizes: m.supportedSizes ?? m.supported_sizes,
    maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
    available: true
  };
}
var DEFAULT_API_URL = "https://blockrun.ai/api";
var DEFAULT_MAX_TOKENS = 1024;
function resolveDefaultTimeout() {
  const envVal = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_CHAT_TIMEOUT : void 0;
  if (envVal !== void 0) {
    const seconds = Number(envVal);
    if (Number.isFinite(seconds) && seconds > 0) {
      return seconds * 1e3;
    }
  }
  return 6e5;
}
var DEFAULT_TIMEOUT = resolveDefaultTimeout();
var LLMClient = class _LLMClient {
  static DEFAULT_API_URL = DEFAULT_API_URL;
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  modelPricingCache = null;
  modelPricingPromise = null;
  // Pre-auth cache: avoids the 402 round-trip on repeat requests to the same model.
  // Key = "endpoint:model", value = cached payment header + timestamp + the
  // length of the request body it was quoted for.
  // TTL: 1 hour — pre-auth quotes are stable server-side on that horizon.
  //
  // A quote prices ONE request (input size + max_tokens), and exact settles
  // the whole signed amount, so reusing it for a smaller request overpaid and
  // for a larger one underpaid. The gateway refuses both with a 402 (an
  // over-signed payment since blockrun#874), so a reuse is only attempted for
  // a body the same size as the one that was quoted.
  preAuthCache = /* @__PURE__ */ new Map();
  static PRE_AUTH_TTL_MS = 36e5;
  /**
   * Initialize the BlockRun LLM client.
   *
   * @param options - Client configuration options (optional if BLOCKRUN_API_KEY, BASE_CHAIN_WALLET_KEY or BLOCKRUN_WALLET_KEY is set)
   */
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BASE_CHAIN_WALLET_KEY || process.env.BLOCKRUN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BASE_CHAIN_WALLET_KEY (or BLOCKRUN_WALLET_KEY) environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts2.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT;
  }
  /**
   * Simple 1-line chat interface.
   *
   * @param model - Model ID (e.g., 'openai/gpt-5.2', 'anthropic/claude-sonnet-4.6')
   * @param prompt - User message
   * @param options - Optional chat parameters
   * @returns Assistant's response text
   *
   * @example
   * const response = await client.chat('gpt-5.2', 'What is the capital of France?');
   * console.log(response); // 'The capital of France is Paris.'
   */
  async chat(model, prompt, options) {
    const messages = [];
    if (options?.system) {
      messages.push({ role: "system", content: options.system });
    }
    messages.push({ role: "user", content: prompt });
    const result = await this.chatCompletion(model, messages, {
      maxTokens: options?.maxTokens,
      temperature: options?.temperature,
      topP: options?.topP,
      search: options?.search,
      searchParameters: options?.searchParameters,
      responseFormat: options?.responseFormat,
      stop: options?.stop,
      fallbackModels: options?.fallbackModels
    });
    return result.choices[0].message.content || "";
  }
  async makeRoutingDecision(prompt, systemPrompt, maxOutputTokens, options = {}) {
    return routeWithCatalog(
      prompt,
      systemPrompt,
      maxOutputTokens,
      await this.getModelPricing(),
      { ...options, minimumPaymentUsd: this.apiAuth ? 0 : BASE_MINIMUM_PAYMENT_USD }
    );
  }
  /**
   * Inspect a local routing decision without making or paying for a model call.
   * The first invocation may fetch the public model catalog for current prices.
   */
  async route(prompt, options) {
    return this.makeRoutingDecision(
      prompt,
      options?.system,
      options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS,
      {
        routingProfile: options?.routingProfile,
        requiresStructuredOutput: options?.responseFormat !== void 0,
        search: options?.search,
        searchParameters: options?.searchParameters
      }
    );
  }
  /**
   * Smart chat with automatic model routing.
   *
   * Uses BlockRun's product-neutral Router Core portfolio strategy: it
   * classifies the task shape locally (no
   * extra model call), enforces capability constraints as hard filters, and
   * ranks an ordered candidate portfolio — the cheapest model that can handle
   * the request wins, and the rest become the transient-error fallback chain.
   *
   * @param prompt - User message
   * @param options - Optional chat and routing parameters
   * @returns SmartChatResponse with response text, selected model, and routing metadata
   *
   * @example Simple usage (auto profile)
   * ```ts
   * const result = await client.smartChat('What is 2+2?');
   * console.log(result.response); // '4'
   * console.log(result.model); // 'google/gemini-3.5-flash'
   * console.log(result.routing.method); // 'portfolio'
   * console.log(result.routing.savings); // 0.78 (78% savings)
   * ```
   *
   * @example With routing profile
   * ```ts
   * // Eco mode (budget optimized)
   * const result = await client.smartChat('Explain quantum computing', { routingProfile: 'eco' });
   *
   * // Premium mode (best quality)
   * const result = await client.smartChat('Write a business plan', { routingProfile: 'premium' });
   * ```
   */
  async smartChat(prompt, options) {
    const decision = await this.route(prompt, options);
    const response = await this.chat(decision.model, prompt, {
      system: options?.system,
      maxTokens: options?.maxTokens,
      temperature: options?.temperature,
      topP: options?.topP,
      search: options?.search,
      searchParameters: options?.searchParameters,
      responseFormat: options?.responseFormat,
      stop: options?.stop,
      // An explicit caller-supplied chain wins over the routed one.
      fallbackModels: options?.fallbackModels ?? decision.fallbacks
    });
    return {
      response,
      model: decision.model,
      routing: decision
    };
  }
  /** Route a full Agent/tool conversation while preserving its complete response. */
  async smartChatCompletion(messages, options = {}) {
    const { prompt, systemPrompt, conversationChars, hasVision } = routingText(messages);
    const decision = await this.makeRoutingDecision(
      prompt,
      systemPrompt,
      options.maxOutputTokens ?? options.maxTokens ?? DEFAULT_MAX_TOKENS,
      {
        routingProfile: options.routingProfile,
        requiresStructuredOutput: options.responseFormat !== void 0,
        tools: options.tools,
        toolChoice: options.toolChoice,
        search: options.search,
        searchParameters: options.searchParameters,
        conversationChars,
        hasVision
      }
    );
    const response = await this.chatCompletion(decision.model, messages, {
      maxTokens: options.maxTokens,
      temperature: options.temperature,
      topP: options.topP,
      search: options.search,
      searchParameters: options.searchParameters,
      tools: options.tools,
      toolChoice: options.toolChoice,
      responseFormat: options.responseFormat,
      stop: options.stop,
      timeout: options.timeout,
      // An explicit caller-supplied chain wins over the routed one.
      fallbackModels: options.fallbackModels ?? decision.fallbacks
    });
    response.routing = decision;
    return { response, model: decision.model, routing: decision };
  }
  /**
   * Get model pricing map (cached).
   * Fetches from API on first call, then returns cached result.
   */
  async getModelPricing() {
    if (this.modelPricingCache) {
      return this.modelPricingCache;
    }
    if (this.modelPricingPromise) {
      return this.modelPricingPromise;
    }
    this.modelPricingPromise = this.fetchModelPricing();
    try {
      this.modelPricingCache = await this.modelPricingPromise;
      return this.modelPricingCache;
    } finally {
      this.modelPricingPromise = null;
    }
  }
  /** Fetch model pricing from the live catalog, preserving flat billing. */
  async fetchModelPricing() {
    const models = await this.listModels();
    const pricing = /* @__PURE__ */ new Map();
    for (const model of models) {
      if (model.available === false) continue;
      const inputPrice = Number.isFinite(model.inputPrice) ? model.inputPrice : 0;
      const outputPrice = Number.isFinite(model.outputPrice) ? model.outputPrice : 0;
      if (model.billingMode === "flat" && Number.isFinite(model.flatPrice) && model.flatPrice > 0) {
        pricing.set(model.id, { inputPrice, outputPrice, flatPrice: model.flatPrice });
      } else {
        pricing.set(model.id, { inputPrice, outputPrice });
      }
    }
    registerSearchModels(pricing, models);
    return pricing;
  }
  /**
   * Full chat completion interface (OpenAI-compatible).
   *
   * When `fallbackModels` is set, transient failures (timeouts, network
   * errors, 429, 5xx) on the primary model trigger a retry against the next
   * model in the list before raising, but only while no payment has been
   * sent for it (the error's {@link RetryDisposition} is `"unpaid"`). Once a
   * payment was sent, any failure propagates: the next model would be a
   * second charge. 4xx errors and PaymentError also propagate immediately —
   * those aren't "swap upstream and retry" situations. Each fallback hop
   * logs one stderr line.
   *
   * @param model - Primary model ID
   * @param messages - Array of messages with role and content
   * @param options - Optional completion parameters
   * @returns ChatResponse object with choices and usage
   */
  async chatCompletion(model, messages, options) {
    const routingProfile = routingProfileForModel(model);
    if (routingProfile) {
      return (await this.smartChatCompletion(messages, {
        ...options,
        routingProfile
      })).response;
    }
    validateMaxTokens(options?.maxTokens);
    const buildBody = (m) => {
      const body = {
        model: m,
        messages,
        max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS
      };
      if (options?.temperature !== void 0) body.temperature = options.temperature;
      if (options?.topP !== void 0) body.top_p = options.topP;
      if (options?.searchParameters !== void 0) {
        body.search_parameters = toWireSearchParameters(options.searchParameters);
      } else if (options?.search === true) {
        body.search_parameters = { mode: "on" };
      }
      if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
      if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
      if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
      if (options?.stop !== void 0) body.stop = options.stop;
      return body;
    };
    const chain = [model, ...options?.fallbackModels ?? []];
    let lastErr;
    for (let i = 0; i < chain.length; i++) {
      const candidate = chain[i];
      try {
        return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
      } catch (err) {
        lastErr = err;
        const next = chain[i + 1];
        if (!next || !isTransientError(err)) throw err;
        console.error(
          `[@blockrun/llm] ${candidate} -> ${next} (${errSummary(err)})`
        );
      }
    }
    throw lastErr;
  }
  /**
   * Write a canonical cost_log entry after a settled x402 payment.
   * Best-effort: failures here must never break a successful API call.
   * Mirrors what Franklin's AgentClient writes via src/agent/llm.ts so
   * cost_log.jsonl is a single source of truth regardless of caller.
   */
  recordCost(url, costUsd, opts) {
    try {
      let endpoint = "";
      try {
        endpoint = new URL(url).pathname;
      } catch {
        endpoint = "";
      }
      const model = opts?.body && typeof opts.body.model === "string" ? opts.body.model : void 0;
      logCost({
        ts: Date.now() / 1e3,
        endpoint,
        cost_usd: costUsd,
        model,
        wallet: this.account.address,
        network: opts?.network,
        client_kind: "LLMClient"
      });
    } catch {
    }
  }
  /**
   * Parse the chat response JSON and attach `fallback` metadata when the
   * gateway signalled a transparent free-fallback substitution. The
   * gateway sets X-Fallback-Used / X-Fallback-Model / X-Settlement-Skipped
   * on the response when it served a paid request from a free model
   * (route.ts createPaymentResponseHeader path). Without surfacing these
   * to the caller, the user gets a different model than requested with
   * no visibility — silent quality drop and no clue why the on-chain
   * balance didn't change.
   */
  async parseChatResponse(response) {
    const body = await response.json();
    const used = response.headers.get("X-Fallback-Used") === "true";
    if (used) {
      body.fallback = {
        used: true,
        model: response.headers.get("X-Fallback-Model") || void 0,
        settlementSkipped: response.headers.get("X-Settlement-Skipped") === "free-fallback"
      };
    }
    return body;
  }
  /**
   * Make a request with automatic x402 payment handling.
   *
   * Every error it throws carries a {@link RetryDisposition}, which is what
   * {@link LLMClient.chatCompletion}'s fallback walk honours: errors of the
   * unpaid first request (and its one 502/503 retry), of the 402 challenge
   * and of signing are `"unpaid"`; anything after the signed payment was sent
   * is `"paid-or-in-doubt"`. With an API key the request itself is billed, so
   * only the account API's explicit 4xx refusal is `"unpaid"`.
   */
  async requestWithPayment(endpoint, body, timeoutMs) {
    const url = `${this.apiUrl}${endpoint}`;
    const unpaid = {
      method: "POST",
      headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
      body: JSON.stringify(body)
    };
    const response = await this.sendUnpaid(url, unpaid, timeoutMs);
    if (response.status === 502 || response.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp = await this.sendUnpaid(url, unpaid, timeoutMs);
      if (retryResp.status !== 502 && retryResp.status !== 503) {
        if (retryResp.status === 402) return this.handlePaymentAndRetry(url, body, retryResp, timeoutMs);
        if (!retryResp.ok) {
          let errorBody;
          try {
            errorBody = await retryResp.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw withDisposition(
            new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody)),
            "unpaid"
          );
        }
        return this.parseUnpaidChatResponse(retryResp);
      }
    }
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response, timeoutMs);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw withDisposition(
        new APIError(
          `API error: ${response.status}`,
          response.status,
          sanitizeErrorResponse(errorBody)
        ),
        "unpaid"
      );
    }
    return this.parseUnpaidChatResponse(response);
  }
  /**
   * Send a request that carries no payment, marking what it throws with its
   * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
   * {@link accountErrorDisposition} (the request itself is billed).
   */
  async sendUnpaid(url, init, timeoutMs) {
    try {
      return await this.fetchWithTimeout(url, init, timeoutMs);
    } catch (err) {
      throw withDisposition(err, this.apiAuth ? accountErrorDisposition(err) : "unpaid");
    }
  }
  /**
   * Parse a response to a request that carried no payment: the free tier,
   * or, with an API key, the billed request itself.
   *
   * With a wallet nothing was paid, so `costUsd` is 0 (a free model). With an
   * API key the call is billed to the account and `costUsd` stays undefined.
   */
  async parseUnpaidChatResponse(response) {
    let served;
    try {
      served = await this.parseChatResponse(response);
    } catch (err) {
      throw withDisposition(err, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
    }
    if (!this.apiAuth) {
      served.costUsd = 0;
      served.settlement = { scheme: "free", quotedUsd: 0 };
    }
    return served;
  }
  /**
   * Handle 402 response: parse requirements, sign payment, retry.
   *
   * Anything thrown before the signed payment is sent is `"unpaid"`; anything
   * after it, `"paid-or-in-doubt"` (see {@link RetryDisposition}).
   */
  async handlePaymentAndRetry(url, body, response, timeoutMs) {
    let signed;
    try {
      signed = await this.signFrom402(url, response);
    } catch (err) {
      throw withDisposition(err, "unpaid");
    }
    try {
      return await this.sendPaid(url, body, signed.paymentPayload, signed.details, timeoutMs);
    } catch (err) {
      throw withDisposition(err, "paid-or-in-doubt");
    }
  }
  /** Read a 402's requirements and sign the exact payment for them. Sends nothing. */
  async signFrom402(url, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch (parseError) {
        console.debug("Failed to parse payment header from response body", parseError);
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const extensions = paymentRequired.extensions;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun AI API call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        asset: details.asset,
        extensions
      }
    );
    return { paymentPayload, details };
  }
  /** Send the signed payment and read the answer. The payment may settle from here on. */
  async sendPaid(url, body, paymentPayload, details, timeoutMs) {
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "User-Agent": USER_AGENT,
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    }, timeoutMs);
    if (retryResponse.status === 502 || retryResponse.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp2 = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "User-Agent": USER_AGENT,
          "PAYMENT-SIGNATURE": paymentPayload
        },
        body: JSON.stringify(body)
      }, timeoutMs);
      if (retryResp2.status !== 502 && retryResp2.status !== 503) {
        if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
        if (!retryResp2.ok) {
          let errorBody;
          try {
            errorBody = await retryResp2.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
        }
        return this.bookPaid(url, body, details, retryResp2);
      }
    }
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retryResponse.status}`,
        retryResponse.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return this.bookPaid(url, body, details, retryResponse);
  }
  /**
   * Book a served, paid chat call and attach its cost. A response whose
   * settlement the gateway skipped ({@link settlementSkipped}: e.g.
   * `X-Settlement-Skipped: free-fallback`) moved nothing on chain, so it is
   * not added to `getSpending()` or the cost log, and reports `costUsd: 0`.
   */
  async bookPaid(url, body, details, response) {
    const costUsd = parseFloat(details.amount) / 1e6;
    if (!settlementSkipped(response)) {
      this.sessionCalls += 1;
      this.sessionTotalUsd += costUsd;
      this.recordCost(url, costUsd, { body, network: details.network });
    }
    return withCost(await this.parseChatResponse(response), response, costUsd, "exact");
  }
  /**
   * Book a served, paid raw (non-chat) call: the signed amount goes to
   * `getSpending()` and the cost log, unless the gateway skipped settlement
   * ({@link settlementSkipped}: e.g. `X-Settlement-Skipped: empty-result` on an
   * empty prediction-market search, `unexecuted` on a JSON-RPC call that never
   * ran, or `X-Payment-Waived`), in which case nothing moved and nothing is
   * booked. `X-Payment-Settled: false` alone is still booked: it is owed.
   */
  bookPaidRaw(url, details, response, body) {
    if (settlementSkipped(response)) return;
    const costUsd = parseFloat(details.amount) / 1e6;
    this.sessionCalls += 1;
    this.sessionTotalUsd += costUsd;
    this.recordCost(url, costUsd, body ? { body, network: details.network } : { network: details.network });
  }
  /**
   * Sign a payment header and return the PAYMENT-SIGNATURE value.
   * Extracted to share logic between streaming and non-streaming flows.
   */
  async signPayment(paymentHeader) {
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const extensions = paymentRequired.extensions;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || `${this.apiUrl}/v1/chat/completions`,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun AI API call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        asset: details.asset,
        extensions
      }
    );
    const costUsd = parseFloat(details.amount) / 1e6;
    return { paymentPayload, costUsd };
  }
  /**
   * Streaming chat completion with automatic x402 payment.
   *
   * Uses a pre-auth cache so repeat calls to the same model skip the 402
   * round-trip (~200ms savings). Falls back to the normal 402 flow on cache
   * miss or if the pre-signed payment is rejected.
   *
   * @returns Raw fetch Response with a streaming SSE body.
   */
  async chatCompletionStream(model, messages, options) {
    validateMaxTokens(options?.maxTokens);
    let requestModel = model;
    const routingProfile = routingProfileForModel(model);
    if (routingProfile) {
      const { prompt, systemPrompt, conversationChars, hasVision } = routingText(messages);
      const decision = await this.makeRoutingDecision(
        prompt,
        systemPrompt,
        options?.maxTokens ?? DEFAULT_MAX_TOKENS,
        {
          routingProfile,
          requiresStructuredOutput: options?.responseFormat !== void 0,
          tools: options?.tools,
          toolChoice: options?.toolChoice,
          conversationChars,
          hasVision
        }
      );
      requestModel = decision.model;
    }
    const url = `${this.apiUrl}/v1/chat/completions`;
    const body = {
      model: requestModel,
      messages,
      max_tokens: options?.maxTokens ?? DEFAULT_MAX_TOKENS,
      stream: true
    };
    if (options?.temperature !== void 0) body.temperature = options.temperature;
    if (options?.topP !== void 0) body.top_p = options.topP;
    if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
    if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
    if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
    if (options?.stop !== void 0) body.stop = options.stop;
    const cacheKey2 = `/v1/chat/completions:${requestModel}`;
    const requestBody = JSON.stringify(body);
    const cached = this.preAuthCache.get(cacheKey2);
    const now = Date.now();
    if (cached && now - cached.cachedAt < _LLMClient.PRE_AUTH_TTL_MS && cached.bodyLength === requestBody.length) {
      let signed;
      try {
        signed = await this.signPayment(cached.paymentHeader);
      } catch {
        this.preAuthCache.delete(cacheKey2);
      }
      if (signed) {
        let preAuthResp;
        try {
          preAuthResp = await this.fetchWithTimeout(url, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              "User-Agent": USER_AGENT,
              "PAYMENT-SIGNATURE": signed.paymentPayload
            },
            body: JSON.stringify(body)
          });
        } catch (err) {
          this.preAuthCache.delete(cacheKey2);
          throw withDisposition(err, "paid-or-in-doubt");
        }
        if (preAuthResp.ok) {
          if (!settlementSkipped(preAuthResp)) {
            this.sessionCalls += 1;
            this.sessionTotalUsd += signed.costUsd;
          }
          return preAuthResp;
        }
        this.preAuthCache.delete(cacheKey2);
        if (preAuthResp.status !== 402) {
          let errorBody;
          try {
            errorBody = await preAuthResp.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw withDisposition(
            new APIError(`API error after payment: ${preAuthResp.status}`, preAuthResp.status, sanitizeErrorResponse(errorBody)),
            "paid-or-in-doubt"
          );
        }
      }
    }
    const firstResp = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
      body: JSON.stringify(body)
    });
    if (firstResp.status !== 402) {
      if (!firstResp.ok) {
        let errorBody;
        try {
          errorBody = await firstResp.json();
        } catch {
          errorBody = { error: "Request failed" };
        }
        throw new APIError(`API error: ${firstResp.status}`, firstResp.status, sanitizeErrorResponse(errorBody));
      }
      return firstResp;
    }
    let paymentHeader = firstResp.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const rb = await firstResp.json();
        if (rb.x402 || rb.accepts) paymentHeader = btoa(JSON.stringify(rb));
      } catch {
      }
    }
    if (!paymentHeader) throw new PaymentError("402 response but no payment requirements found");
    this.preAuthCache.set(cacheKey2, { paymentHeader, cachedAt: now, bodyLength: requestBody.length });
    const { paymentPayload, costUsd } = await this.signPayment(paymentHeader);
    const streamResp = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "User-Agent": USER_AGENT,
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (streamResp.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
    if (!streamResp.ok) {
      let errorBody;
      try {
        errorBody = await streamResp.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error after payment: ${streamResp.status}`, streamResp.status, sanitizeErrorResponse(errorBody));
    }
    if (!settlementSkipped(streamResp)) {
      this.sessionCalls += 1;
      this.sessionTotalUsd += costUsd;
    }
    return streamResp;
  }
  /**
   * Make a request with automatic x402 payment handling, returning raw JSON.
   * Used for non-ChatResponse endpoints (search, image edit, etc.).
   */
  async requestWithPaymentRaw(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
      body: JSON.stringify(body)
    });
    if (response.status === 502 || response.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
        body: JSON.stringify(body)
      });
      if (retryResp.status !== 502 && retryResp.status !== 503) {
        if (retryResp.status === 402) return this.handlePaymentAndRetryRaw(url, body, retryResp);
        if (!retryResp.ok) {
          let errorBody;
          try {
            errorBody = await retryResp.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
        }
        return retryResp.json();
      }
    }
    if (response.status === 402) {
      return this.handlePaymentAndRetryRaw(url, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    if (this.apiAuth && endpoint.startsWith("/v1/images/")) return this.apiAuth.poll(response, this.timeout, 2e3);
    return response.json();
  }
  /**
   * Handle 402 response for raw endpoints: parse requirements, sign payment, retry.
   */
  async handlePaymentAndRetryRaw(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
        console.debug("Failed to parse payment header from response body");
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const extensions = paymentRequired.extensions;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || url,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun AI API call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        asset: details.asset,
        extensions
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "User-Agent": USER_AGENT,
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 502 || retryResponse.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp2 = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "User-Agent": USER_AGENT,
          "PAYMENT-SIGNATURE": paymentPayload
        },
        body: JSON.stringify(body)
      });
      if (retryResp2.status !== 502 && retryResp2.status !== 503) {
        if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
        if (!retryResp2.ok) {
          let errorBody;
          try {
            errorBody = await retryResp2.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
        }
        this.bookPaidRaw(url, details, retryResp2, body);
        return retryResp2.json();
      }
    }
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retryResponse.status}`,
        retryResponse.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    this.bookPaidRaw(url, details, retryResponse, body);
    return retryResponse.json();
  }
  /**
   * GET with automatic x402 payment handling, returning raw JSON.
   * Used for Predexon prediction market endpoints that use GET + query params.
   */
  async getWithPaymentRaw(endpoint, params) {
    const query = params ? "?" + new URLSearchParams(params).toString() : "";
    const url = `${this.apiUrl}${endpoint}${query}`;
    const response = await this.fetchWithTimeout(url, {
      method: "GET",
      headers: { "User-Agent": USER_AGENT }
    });
    if (response.status === 502 || response.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp = await this.fetchWithTimeout(url, {
        method: "GET",
        headers: { "User-Agent": USER_AGENT }
      });
      if (retryResp.status !== 502 && retryResp.status !== 503) {
        if (retryResp.status === 402) return this.handleGetPaymentAndRetryRaw(url, endpoint, params, retryResp);
        if (!retryResp.ok) {
          let errorBody;
          try {
            errorBody = await retryResp.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw new APIError(`API error: ${retryResp.status}`, retryResp.status, sanitizeErrorResponse(errorBody));
        }
        return retryResp.json();
      }
    }
    if (response.status === 402) {
      return this.handleGetPaymentAndRetryRaw(url, endpoint, params, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return response.json();
  }
  /**
   * Handle 402 response for GET endpoints: parse requirements, sign payment, retry with GET.
   */
  async handleGetPaymentAndRetryRaw(url, endpoint, params, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
        console.debug("Failed to parse payment header from response body");
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const extensions = paymentRequired.extensions;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || url,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun AI API call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        asset: details.asset,
        extensions
      }
    );
    const query = params ? "?" + new URLSearchParams(params).toString() : "";
    const retryUrl = `${this.apiUrl}${endpoint}${query}`;
    const retryResponse = await this.fetchWithTimeout(retryUrl, {
      method: "GET",
      headers: {
        "User-Agent": USER_AGENT,
        "PAYMENT-SIGNATURE": paymentPayload
      }
    });
    if (retryResponse.status === 502 || retryResponse.status === 503) {
      await new Promise((r) => setTimeout(r, 1e3));
      const retryResp2 = await this.fetchWithTimeout(retryUrl, {
        method: "GET",
        headers: {
          "User-Agent": USER_AGENT,
          "PAYMENT-SIGNATURE": paymentPayload
        }
      });
      if (retryResp2.status !== 502 && retryResp2.status !== 503) {
        if (retryResp2.status === 402) throw new PaymentError("Payment was rejected. Check your wallet balance.");
        if (!retryResp2.ok) {
          let errorBody;
          try {
            errorBody = await retryResp2.json();
          } catch {
            errorBody = { error: "Request failed" };
          }
          throw new APIError(`API error after payment: ${retryResp2.status}`, retryResp2.status, sanitizeErrorResponse(errorBody));
        }
        this.bookPaidRaw(url, details, retryResp2);
        return retryResp2.json();
      }
    }
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retryResponse.status}`,
        retryResponse.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    this.bookPaidRaw(url, details, retryResponse);
    return retryResponse.json();
  }
  /**
   * Fetch with timeout: `timeoutMs` (a per-call override), else the client's `timeout`.
   */
  async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
        ...options,
        signal: controller.signal
      });
      return response;
    } finally {
      clearTimeout(timeoutId);
    }
  }
  /**
   * List available models with pricing.
   *
   * Returns the full `/v1/models` unified catalog (chat + image + music).
   * The shape preserves backwards compatibility — image/music rows have
   * `inputPrice = outputPrice = 0` since those fields don't apply, and
   * their per-call price surfaces via `flatPrice`.
   */
  async listModels() {
    const raw = await this.fetchRawModels();
    return raw.map((m) => mapRawToModel(m));
  }
  /**
   * List available image generation models with pricing.
   *
   * The dedicated `/v1/images/models` endpoint was deprecated server-side;
   * image models live in the unified `/v1/models` catalog under
   * `categories: ["image", ...]`. This method filters that catalog so
   * existing callers keep working.
   */
  async listImageModels() {
    const raw = await this.fetchRawModels();
    return raw.filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => mapRawToImageModel(m));
  }
  /**
   * List all available models (chat, image, music, etc.) with pricing.
   *
   * @returns Array of all models with `type` field set from category
   * (`llm` for chat, `image` / `music` for media). Backwards-compat:
   * chat models always report `type: "llm"`.
   */
  async listAllModels() {
    const raw = await this.fetchRawModels();
    const out = [];
    for (const m of raw) {
      const cats = Array.isArray(m.categories) ? m.categories : [];
      if (cats.includes("image")) {
        const model = mapRawToImageModel(m);
        model.type = "image";
        out.push(model);
      } else {
        const model = mapRawToModel(m);
        model.type = "llm";
        out.push(model);
      }
    }
    return out;
  }
  /**
   * Internal: fetch the raw `/v1/models` catalog without normalising shape.
   * Used by listImageModels / listAllModels so each can pick category-
   * specific fields.
   */
  async fetchRawModels() {
    const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, {
      method: "GET"
    });
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `Failed to list models: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    const data = await response.json();
    return data.data || [];
  }
  /**
   * Edit an image using img2img, optionally fusing multiple source images.
   *
   * @param prompt - Text description of the desired edit
   * @param image - A base64 `data:image/...` URI, or an array of 2–4 such URIs
   *   to fuse (image fusion). Caps: `openai/*` up to 4, `google/*` up to 3.
   *   A `mask` cannot be combined with multiple source images.
   * @param options - Optional edit parameters
   * @returns ImageResponse with edited image URLs
   */
  async imageEdit(prompt, image, options) {
    const body = {
      model: options?.model || "openai/gpt-image-2",
      prompt,
      image,
      size: options?.size || "1024x1024",
      n: options?.n || 1
    };
    if (options?.mask !== void 0) {
      body.mask = options.mask;
    }
    const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
    return data;
  }
  /**
   * Standalone search over web and news. For X/Twitter, use chat on an
   * `xai/*` model with an `x_search` tool.
   *
   * @param query - Search query
   * @param options - Optional search parameters
   * @returns SearchResult with summary and citations
   */
  async search(query, options) {
    const body = {
      query,
      max_results: options?.maxResults || 10
    };
    if (options?.sources !== void 0) body.sources = options.sources;
    if (options?.fromDate !== void 0) body.from_date = options.fromDate;
    if (options?.toDate !== void 0) body.to_date = options.toDate;
    const data = await this.requestWithPaymentRaw("/v1/search", body);
    return data;
  }
  /**
   * Generic Exa endpoint proxy (POST). Useful when you need an Exa API
   * surface that the typed wrappers below don't expose.
   *
   * @param path - Exa endpoint segment: "search" | "find-similar" | "contents" | "answer"
   * @param body - Request body (see Exa API docs)
   *
   * @example
   * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
   */
  async exa(path7, body) {
    return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
  }
  /**
   * Neural web search via Exa. Returns semantically relevant URLs and metadata.
   * Understands meaning, not just keywords.
   *
   * @param query - Natural language search query
   * @param options - Optional filters (numResults, category, date range, domains)
   */
  async exaSearch(query, options) {
    const body = { query };
    if (options?.numResults !== void 0) body.numResults = options.numResults;
    if (options?.category !== void 0) body.category = options.category;
    if (options?.startPublishedDate !== void 0) body.startPublishedDate = options.startPublishedDate;
    if (options?.endPublishedDate !== void 0) body.endPublishedDate = options.endPublishedDate;
    if (options?.includeDomains !== void 0) body.includeDomains = options.includeDomains;
    if (options?.excludeDomains !== void 0) body.excludeDomains = options.excludeDomains;
    const data = await this.requestWithPaymentRaw("/v1/exa/search", body);
    return data.data;
  }
  /**
   * Ask a question and get a cited, synthesized answer grounded in real web sources.
   * No hallucinations — every claim is backed by a citation.
   *
   * @param query - The question to answer
   */
  async exaAnswer(query) {
    const data = await this.requestWithPaymentRaw("/v1/exa/answer", { query });
    return data.data;
  }
  /**
   * Fetch full Markdown text content from a list of URLs. Billed per URL.
   * Returns clean text ready to feed into an LLM context window.
   *
   * @param urls - Array of URLs to fetch (up to 100)
   */
  async exaContents(urls) {
    const data = await this.requestWithPaymentRaw("/v1/exa/contents", { urls });
    return data.data;
  }
  /**
   * Find pages semantically similar to a given URL.
   * Useful for discovering competitors, alternatives, and related resources.
   *
   * @param url - Reference URL
   * @param options - Optional filters (numResults, excludeSourceDomain)
   */
  async exaFindSimilar(url, options) {
    const body = { url };
    if (options?.numResults !== void 0) body.numResults = options.numResults;
    if (options?.excludeSourceDomain !== void 0) body.excludeSourceDomain = options.excludeSourceDomain;
    const data = await this.requestWithPaymentRaw("/v1/exa/find-similar", body);
    return data.data;
  }
  /**
   * Mint a one-time Coinbase Onramp link to fund this wallet with USDC on Base.
   *
   * FREE — the x402 signature only authenticates the wallet (no payment is
   * charged). Because the funding address must equal the signing wallet, this
   * mints a `pay.coinbase.com` link prefilled for `address` so a card/bank can
   * top it up (60+ fiat currencies → Base USDC). Base / USDC only.
   *
   * The returned URL is single-use and expires in ~5 minutes — mint it at click
   * time and never cache it. The funding `address` must match the signing wallet.
   *
   * @param address - Destination Base wallet address (0x + 40 hex chars). Must
   *                  equal the signing wallet that funds it.
   * @returns `{ url }` — a one-time https://pay.coinbase.com/... link.
   *
   * @example
   * const { url } = await client.onramp(client.getWalletAddress());
   * console.log(`Fund your wallet: ${url}`);
   */
  async onramp(address) {
    if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
      throw new Error("address must be a 0x-prefixed 40-hex-character Base address");
    }
    const data = await this.requestWithPaymentRaw("/v1/onramp/token", {
      address,
      network: "base",
      asset: "USDC"
    });
    const url = typeof data.url === "string" ? data.url : "";
    if (!url.startsWith("https://pay.coinbase.com/")) {
      throw new APIError("gateway returned no onramp url", 502, { message: "gateway returned no onramp url" });
    }
    return { url };
  }
  /**
   * Get USDC balance on Base mainnet.
   *
   * @returns USDC balance as a float (6 decimal places normalized)
   *
   * @example
   * const balance = await client.getBalance();
   * console.log(`Balance: $${balance.toFixed(2)} USDC`);
   */
  async getBalance() {
    const usdcContract = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
    const configuredRpc = typeof process !== "undefined" && process.env ? process.env.BASE_RPC_URL : void 0;
    const rpcs = [
      configuredRpc,
      "https://base-rpc.publicnode.com",
      "https://mainnet.base.org",
      "https://base.llamarpc.com"
    ].filter((rpc) => Boolean(rpc));
    const selector = "0x70a08231";
    const paddedAddress = this.account.address.slice(2).toLowerCase().padStart(64, "0");
    const data = selector + paddedAddress;
    const payload = {
      jsonrpc: "2.0",
      method: "eth_call",
      params: [{ to: usdcContract, data }, "latest"],
      id: 1
    };
    let lastError;
    for (const rpc of rpcs) {
      try {
        const response = await fetch(rpc, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(payload)
        });
        if (!response.ok) throw new Error(`Base RPC returned ${response.status}`);
        const result = await response.json();
        if (!result.result || result.error) throw new Error("Base RPC returned no balance result");
        const balanceRaw = parseInt(result.result || "0x0", 16);
        return balanceRaw / 1e6;
      } catch (e) {
        lastError = e;
      }
    }
    throw lastError || new Error("All RPCs failed");
  }
  /**
   * Query Predexon prediction market data (GET endpoints).
   *
   * Access real-time data from Polymarket, Kalshi, Limitless, Opinion, Predict.Fun, Binance Futures, UMA and sports markets.
   * Powered by Predexon. Billed per request.
   *
   * @param path - Endpoint path, e.g. "polymarket/events", "kalshi/markets/12345"
   * @param params - Query parameters passed to the endpoint
   *
   * @example
   * const events = await client.pm("polymarket/events");
   * const market = await client.pm("kalshi/markets/KXBTC-25MAR14");
   * const results = await client.pm("polymarket/search", { q: "bitcoin" });
   */
  async pm(path7, params) {
    return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
  }
  /**
   * Structured query for Predexon prediction market data (POST endpoints).
   *
   * For endpoints that require a JSON body, e.g. bulk wallet identity lookup.
   * Tier 1 =, Tier 2 =
   *
   * @param path - Endpoint path, e.g. "polymarket/wallet/identities"
   * @param query - JSON body for the structured query
   *
   * @example
   * const batch = await client.pmQuery("polymarket/wallet/identities", {
   *   addresses: ["0xabc...", "0xdef..."],
   * });
   */
  async pmQuery(path7, query) {
    return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
  }
  // ── PM convenience helpers (Predexon v2) ─────────────────────────────────
  // Thin wrappers over pm() / pmQuery() for the most common v2 endpoints.
  /** RETIRED — `/v1/pm/markets` no longer exists.
   *
   * Predexon sunset market matching on 2026-07-20 and the whole canonical layer
   * went with it, so this path returns 410 upstream. Use
   * `pm("markets/search", { q })` for cross-venue lookups.
   *
   * Kept as a throwing stub rather than deleted so upgrading does not break
   * property access; it throws before any network I/O.
   *
   * @throws {RetiredEndpointError} always. */
  async pmMarkets(_params) {
    throw new RetiredEndpointError(
      '/v1/pm/markets was sunset by Predexon on 2026-07-20 (upstream 410). Use pm("markets/search", { q }) for cross-venue lookups.'
    );
  }
  /** RETIRED — `/v1/pm/markets/listings` no longer exists (410, sunset
   * 2026-07-20 with market matching). Use `pm("markets/search", { q })`.
   *
   * @throws {RetiredEndpointError} always. */
  async pmListings(_params) {
    throw new RetiredEndpointError(
      '/v1/pm/markets/listings was sunset by Predexon on 2026-07-20 (upstream 410). Use pm("markets/search", { q }) for cross-venue lookups.'
    );
  }
  /** RETIRED — `/v1/pm/outcomes/{predexonId}` no longer exists (410, sunset
   * 2026-07-20 with market matching). Use `pm("markets/search", { q })`.
   *
   * @throws {RetiredEndpointError} always. */
  async pmOutcome(_predexonId) {
    throw new RetiredEndpointError(
      '/v1/pm/outcomes/{predexon_id} was sunset by Predexon on 2026-07-20 (upstream 410). Use pm("markets/search", { q }) for cross-venue lookups.'
    );
  }
  /** Polymarket markets with cursor-based keyset pagination (use pagination_key).
   * Tier 1. */
  async pmPolymarketMarketsKeyset(params) {
    return this.pm("polymarket/markets/keyset", params);
  }
  /** Polymarket events with cursor-based keyset pagination (use pagination_key).
   * Tier 1. */
  async pmPolymarketEventsKeyset(params) {
    return this.pm("polymarket/events/keyset", params);
  }
  /** List available sports categories. Tier 1.
   *
   * NOTE: upstream returns 500 for every `sports/*` path as of 2026-08-04.
   * The route still resolves, so this works again the moment Predexon restores
   * it, but do not build on it yet. */
  async pmSportsCategories() {
    return this.pm("sports/categories");
  }
  /** List sports markets grouped by game. Filter with league, sport_type,
   * status, venue. Tier 1.
   *
   * NOTE: upstream returns 500 for every `sports/*` path as of 2026-08-04. */
  async pmSportsMarkets(params) {
    return this.pm("sports/markets", params);
  }
  /** Fetch identity + profile metadata for one wallet (ENS, Twitter, portfolio,
   * etc.). Tier 2. */
  async pmWalletIdentity(wallet) {
    return this.pm(`polymarket/wallet/identity/${wallet}`);
  }
  /** Bulk identity lookup for up to 200 wallet addresses (POST). Tier 2. */
  async pmWalletIdentities(addresses) {
    return this.pmQuery("polymarket/wallet/identities", { addresses });
  }
  /** Discover wallets connected to a seed address via on-chain transfers and
   * identity proofs. Tier 2. */
  async pmWalletCluster(address) {
    return this.pm(`polymarket/wallet/${address}/cluster`);
  }
  // ============================================================
  // DefiLlama (DeFi protocols / TVL / yields / prices)
  // ============================================================
  /**
   * Query DefiLlama DeFi data (GET passthrough). Powered by DefiLlama.
   * for protocols / protocol/{slug} / chains / yields;
   * for prices/{coins}.
   *
   * @param path - e.g. "protocols", "protocol/aave", "chains", "yields",
   *               "prices/coingecko:bitcoin,base:0x..."
   * @param params - Query parameters passed through to DefiLlama
   */
  async defi(path7, params) {
    return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
  }
  /** All DeFi protocols with TVL. */
  async defiProtocols() {
    return this.defi("protocols");
  }
  /** Single protocol details + historical TVL. */
  async defiProtocol(slug) {
    return this.defi(`protocol/${slug}`);
  }
  /** Current TVL of every chain. */
  async defiChains() {
    return this.defi("chains");
  }
  /** Yield pools with APY/TVL. */
  async defiYields(params) {
    return this.defi("yields", params);
  }
  /** Token price lookup. Coins like "coingecko:bitcoin" or "{chain}:{address}". */
  async defiPrices(coins) {
    const joined = Array.isArray(coins) ? coins.join(",") : coins;
    return this.defi(`prices/${joined}`);
  }
  // ============================================================
  // 0x DEX (swap quotes + gasless) — free passthrough
  // ============================================================
  /**
   * Query the 0x Swap / Gasless APIs (free — no x402 payment; BlockRun
   * takes an on-chain affiliate fee on executed swaps instead).
   *
   * @param path - "price", "quote", "gasless/price", "gasless/quote",
   *               "gasless/submit" (POST), "gasless/status/{hash}",
   *               "gasless/approval-tokens", "gasless/chains", "swap/chains"
   * @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
   * @param body - JSON body — pass to switch to POST (gasless/submit only)
   */
  async dex(path7, params, body) {
    if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
    return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
  }
  /** Indicative Permit2 swap price — no commitment (free). */
  async dexPrice(params) {
    return this.dex("price", params);
  }
  /** Firm Permit2 swap quote with permit2.eip712 + tx data (free). */
  async dexQuote(params) {
    return this.dex("quote", params);
  }
  /** Gasless indicative price quote (free). */
  async dexGaslessPrice(params) {
    return this.dex("gasless/price", params);
  }
  /** Gasless firm quote — returns trade.eip712 to sign (free). */
  async dexGaslessQuote(params) {
    return this.dex("gasless/quote", params);
  }
  /** Submit a signed gasless trade; the 0x relayer pays gas (free). */
  async dexGaslessSubmit(body) {
    return this.dex("gasless/submit", void 0, body);
  }
  /** Poll a gasless trade's status by tradeHash (free). */
  async dexGaslessStatus(tradeHash) {
    return this.dex(`gasless/status/${tradeHash}`);
  }
  /** Chains where the Swap API is supported (free). */
  async dexChains() {
    return this.dex("swap/chains");
  }
  /** Chains where the Gasless API is supported (free). */
  async dexGaslessChains() {
    return this.dex("gasless/chains");
  }
  // ============================================================
  // Modal Sandbox (pay-per-call cloud compute)
  // ============================================================
  /**
   * Call the Modal sandbox compute API (POST passthrough).
   *
   * @param path - "sandbox/create" (CPU / GPU tiers), "sandbox/exec",
   *               "sandbox/status", "sandbox/terminate"
   * @param body - JSON body for the endpoint
   */
  async modal(path7, body) {
    return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
  }
  /** Create a sandboxed compute environment (CPU / GPU tiers). */
  async modalSandboxCreate(body) {
    return this.modal("sandbox/create", body);
  }
  /** Execute a command in a sandbox; returns stdout/stderr. */
  async modalSandboxExec(sandboxId, command, extra) {
    return this.modal("sandbox/exec", { sandbox_id: sandboxId, command, ...extra });
  }
  /** Check a sandbox's status. */
  async modalSandboxStatus(sandboxId) {
    return this.modal("sandbox/status", { sandbox_id: sandboxId });
  }
  /** Terminate a sandbox. */
  async modalSandboxTerminate(sandboxId) {
    return this.modal("sandbox/terminate", { sandbox_id: sandboxId });
  }
  /**
   * Get current session spending.
   *
   * @returns Object with totalUsd and calls count
   *
   * @example
   * const spending = client.getSpending();
   * console.log(`Spent $${spending.totalUsd.toFixed(4)} across ${spending.calls} calls`);
   */
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return {
      totalUsd: this.sessionTotalUsd,
      calls: this.sessionCalls
    };
  }
  /**
   * Get the wallet address being used for payments.
   */
  getWalletAddress() {
    return this.account.address;
  }
};
var client_default = LLMClient;

// src/image.ts
var import_accounts3 = require("viem/accounts");
var DEFAULT_API_URL2 = "https://blockrun.ai/api";
var DEFAULT_MODEL = "google/nano-banana";
var DEFAULT_SIZE = "1024x1024";
var DEFAULT_TIMEOUT2 = 2e5;
var POLL_INTERVAL_MS = 3e3;
var POLL_MAX_DURATION_MS = 3e5;
var ImageClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  /**
   * Initialize the BlockRun Image client.
   *
   * @param options - Client configuration options
   */
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts3.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL2;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT2;
  }
  /**
   * Generate an image from a text prompt.
   *
   * @param prompt - Text description of the image to generate
   * @param options - Optional generation parameters
   * @returns ImageResponse with generated image URLs
   *
   * @example
   * const result = await client.generate('A sunset over mountains');
   * console.log(result.data[0].url);
   */
  async generate(prompt, options) {
    const body = {
      model: options?.model || DEFAULT_MODEL,
      prompt,
      size: options?.size || DEFAULT_SIZE,
      n: options?.n || 1
    };
    if (options?.quality) {
      body.quality = options.quality;
    }
    return this.requestWithPayment("/v1/images/generations", body);
  }
  /**
   * Edit an image using img2img, optionally fusing multiple source images.
   *
   * Pass a single `data:image/...` base64 data URI to edit one image, or an
   * array of 2–4 data URIs to fuse them (image fusion) — e.g. a reference photo
   * plus a brand logo. The server caps fusion per provider: `openai/*` accepts
   * up to 4 source images, `google/*` (Nano Banana) up to 3. A `mask` cannot be
   * combined with multiple source images.
   *
   * Edit-capable models: `openai/gpt-image-1`, `openai/gpt-image-2`,
   * `openai/gpt-image-2.5-sunburst`, `google/nano-banana`, `google/nano-banana-2`,
   * `google/nano-banana-pro`. `mask` is OpenAI-only.
   *
   * @param prompt - Text description of the desired edit
   * @param image - A base64 `data:image/...` URI, or an array of 2–4 such URIs to fuse
   * @param options - Optional edit parameters
   * @returns ImageResponse with edited image URLs
   *
   * @example
   * // Single-image edit
   * const result = await client.edit('Make it a painting', imageDataUri);
   *
   * @example
   * // Multi-image fusion (Nano Banana): blend a subject with a brand logo
   * const fused = await client.edit(
   *   'Place the logo on the t-shirt',
   *   [subjectDataUri, logoDataUri],
   *   { model: 'google/nano-banana' }
   * );
   */
  async edit(prompt, image, options) {
    const body = {
      model: options?.model || "openai/gpt-image-2",
      prompt,
      image,
      size: options?.size || "1024x1024",
      n: options?.n || 1
    };
    if (options?.mask !== void 0) {
      body.mask = options.mask;
    }
    return this.requestWithPayment("/v1/images/image2image", body);
  }
  /**
   * List available image generation models with pricing.
   *
   * The dedicated `/v1/images/models` endpoint was deprecated server-side;
   * image models live in the unified `/v1/models` catalog under
   * `categories: ["image", ...]`. This method filters that catalog so
   * existing callers keep working.
   */
  async listImageModels() {
    const response = await this.fetchWithTimeout(
      `${this.apiUrl}/v1/models`,
      { method: "GET" }
    );
    if (!response.ok) {
      throw new APIError(
        `Failed to list image models: ${response.status}`,
        response.status
      );
    }
    const data = await response.json();
    return (data.data || []).filter((m) => Array.isArray(m.categories) && m.categories.includes("image")).map((m) => ({
      id: m.id,
      name: m.name || m.id,
      provider: m.provider || m.owned_by || "",
      description: m.description || "",
      pricePerImage: m.pricePerImage ?? m.price_per_image ?? m.pricing?.per_image ?? m.pricing?.perImage ?? m.pricing?.flat ?? m.flatPrice ?? m.flat_price ?? 0,
      supportedSizes: m.supportedSizes ?? m.supported_sizes,
      maxPromptLength: m.maxPromptLength ?? m.max_prompt_length,
      available: true
    }));
  }
  /**
   * Make a request with automatic x402 payment handling.
   */
  async requestWithPayment(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    if (this.apiAuth) return this.apiAuth.poll(response, POLL_MAX_DURATION_MS, POLL_INTERVAL_MS);
    return response.json();
  }
  /**
   * Handle 402 response: parse requirements, sign payment, retry.
   */
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const extensions = paymentRequired.extensions;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || `${this.apiUrl}/v1/images/generations`,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun Image Generation",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        asset: details.asset,
        extensions
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 402) {
      throw new PaymentError(
        "Payment was rejected. Check your wallet balance."
      );
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retryResponse.status}`,
        retryResponse.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    const responseBody = await retryResponse.json();
    if (retryResponse.status === 202 || responseBody.status === "queued" || responseBody.status === "in_progress") {
      if (!responseBody.poll_url) {
        throw new APIError(
          `Server returned ${retryResponse.status} but no poll_url to follow.`,
          retryResponse.status,
          sanitizeErrorResponse(responseBody)
        );
      }
      return this.pollImageJob(responseBody.poll_url, paymentPayload);
    }
    return responseBody;
  }
  /**
   * Poll the async image generation endpoint until the job reaches a terminal
   * state (completed/failed). Used when the POST returns 202 + poll_url for
   * slow models that exceeded the server's inline window.
   *
   * Sends the SAME payment header on every poll — the server binds the payer
   * to the job id and re-verifies on each call. Settlement happens server-side
   * on the first poll where status=completed.
   */
  async pollImageJob(pollPath, paymentHeader) {
    const pollUrl = pollPath.startsWith("/api/") ? `${this.apiUrl.replace(/\/api$/, "")}${pollPath}` : `${this.apiUrl}${pollPath.startsWith("/") ? "" : "/"}${pollPath}`;
    const startedAt = Date.now();
    while (Date.now() - startedAt < POLL_MAX_DURATION_MS) {
      await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
      const pollResp = await this.fetchWithTimeout(pollUrl, {
        method: "GET",
        headers: {
          "PAYMENT-SIGNATURE": paymentHeader,
          "X-PAYMENT": paymentHeader
        }
      });
      if (pollResp.status === 202) {
        continue;
      }
      const pollBody = await pollResp.json().catch(() => ({}));
      if (!pollResp.ok) {
        throw new APIError(
          `API error during poll: ${pollResp.status}`,
          pollResp.status,
          sanitizeErrorResponse(pollBody)
        );
      }
      if (pollBody.status === "failed") {
        throw new APIError(
          pollBody.error || "Upstream image generation failed",
          200,
          sanitizeErrorResponse(pollBody)
        );
      }
      if (pollBody.data) {
        return pollBody;
      }
      if (pollBody.status === "queued" || pollBody.status === "in_progress") {
        continue;
      }
    }
    throw new APIError(
      `Image generation poll timed out after ${POLL_MAX_DURATION_MS / 1e3}s`,
      504,
      { error: "poll_timeout", poll_url: pollPath }
    );
  }
  /**
   * Fetch with timeout.
   */
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      const response = await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, {
        ...options,
        signal: controller.signal
      });
      return response;
    } finally {
      clearTimeout(timeoutId);
    }
  }
  /**
   * Get the wallet address being used for payments.
   */
  getWalletAddress() {
    return this.account.address;
  }
  /**
   * Get session spending information.
   */
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return {
      totalUsd: this.sessionTotalUsd,
      calls: this.sessionCalls
    };
  }
};

// src/music.ts
var import_accounts4 = require("viem/accounts");
var DEFAULT_API_URL3 = "https://blockrun.ai/api";
var DEFAULT_MODEL2 = "minimax/music-2.5+";
var DEFAULT_TIMEOUT3 = 21e4;
var MusicClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts4.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL3;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT3;
  }
  /**
   * Generate a music track from a text prompt.
   *
   * Takes 1-3 minutes. Returns a CDN URL valid for ~24h.
   *
   * @param prompt - Music style, mood, or description
   * @param options - Optional generation parameters
   * @returns MusicResponse with track URL and metadata
   *
   * @example
   * const result = await client.generate('chill lo-fi beats with piano');
   * console.log(result.data[0].url); // Download this URL — expires in 24h
   *
   * @example With lyrics
   * const result = await client.generate('upbeat pop song', {
   *   instrumental: false,
   *   lyrics: 'Hello world, this is my song...'
   * });
   */
  async generate(prompt, options) {
    const instrumental = options?.instrumental ?? true;
    const lyrics = options?.lyrics?.trim();
    if (instrumental && lyrics) {
      throw new Error("Cannot specify lyrics when instrumental is true");
    }
    const body = {
      model: options?.model || DEFAULT_MODEL2,
      prompt,
      instrumental
    };
    if (lyrics) body.lyrics = lyrics;
    return this.requestWithPayment("/v1/audio/generations", body);
  }
  async requestWithPayment(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    if (this.apiAuth) return this.apiAuth.poll(response, this.timeout, 5e3);
    return response.json();
  }
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || `${this.apiUrl}/v1/audio/generations`,
        resourceDescription: details.resource?.description || "BlockRun Music Generation",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
    }
    const data = await retryResponse.json();
    if (!settlementSkipped(retryResponse)) {
      this.sessionCalls++;
      this.sessionTotalUsd += 0.1575;
    }
    const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
    if (txHash) data.txHash = txHash;
    return data;
  }
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};

// src/speech.ts
var import_accounts5 = require("viem/accounts");
var DEFAULT_API_URL4 = "https://blockrun.ai/api";
var DEFAULT_MODEL3 = "elevenlabs/flash-v2.5";
var DEFAULT_SOUNDFX_MODEL = "elevenlabs/sound-effects";
var DEFAULT_TIMEOUT4 = 12e4;
var SpeechClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts5.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL4;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT4;
  }
  /**
   * Synthesize speech from text (OpenAI-compatible TTS).
   *
   * Price scales with character count: (chars / 1000) x model rate,
   * minimum Synthesis is synchronous.
   *
   * @param input - Text to synthesize. Per-model character caps apply
   *                (flash/turbo 40k, multilingual-v2 10k, v3 5k).
   * @param options - Optional model / voice / format / speed
   * @returns SpeechResponse with audio URL, format, and character count
   *
   * @example
   * const result = await client.generate('Welcome to BlockRun.', { voice: 'george' });
   * console.log(result.data[0].url); // audio URL (mp3 by default)
   */
  async generate(input, options) {
    const body = {
      model: options?.model || DEFAULT_MODEL3,
      input
    };
    if (options?.voice) body.voice = options.voice;
    if (options?.responseFormat) body.response_format = options.responseFormat;
    if (options?.speed !== void 0) body.speed = options.speed;
    return this.requestWithPayment("/v1/audio/speech", body);
  }
  /** OpenAI-style alias for {@link generate}. */
  speak(input, options) {
    return this.generate(input, options);
  }
  /**
   * Generate a cinematic sound effect from a text prompt.
   *
   *, up to 22 seconds of audio.
   *
   * @param text - Sound effect description (max 1000 chars).
   *               E.g. 'rain on a tin roof', 'sci-fi door whoosh'
   * @param options - Optional duration / prompt influence / format
   * @returns SpeechResponse with audio URL and format
   *
   * @example
   * const fx = await client.soundEffect('crackling campfire at night');
   * console.log(fx.data[0].url);
   */
  async soundEffect(text, options) {
    const body = {
      model: options?.model || DEFAULT_SOUNDFX_MODEL,
      text
    };
    if (options?.durationSeconds !== void 0) body.duration_seconds = options.durationSeconds;
    if (options?.promptInfluence !== void 0) body.prompt_influence = options.promptInfluence;
    if (options?.responseFormat) body.response_format = options.responseFormat;
    return this.requestWithPayment("/v1/audio/sound-effects", body);
  }
  /**
   * List available voices for TTS (free, rate-limited 60 req/min/IP).
   *
   * Pass a voice's `alias` (if present) or `voice_id` as the `voice`
   * option to {@link generate}.
   */
  async listVoices() {
    const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/audio/voices`, {
      method: "GET"
    });
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    const payload = await response.json();
    return payload.data || [];
  }
  async requestWithPayment(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, endpoint, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    return response.json();
  }
  async handlePaymentAndRetry(url, endpoint, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || `${this.apiUrl}${endpoint}`,
        resourceDescription: details.resource?.description || "BlockRun Voice",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
    }
    const data = await retryResponse.json();
    if (!settlementSkipped(retryResponse)) {
      this.sessionCalls++;
      const paidUsd = Number(details.amount) / 1e6;
      if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
    }
    const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
    if (txHash) data.txHash = txHash;
    return data;
  }
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};

// src/video.ts
var import_accounts6 = require("viem/accounts");
var DEFAULT_API_URL5 = "https://blockrun.ai/api";
var DEFAULT_MODEL4 = "xai/grok-imagine-video";
var DEFAULT_TIMEOUT5 = 12e4;
var POLL_INTERVAL_MS2 = 5e3;
var DEFAULT_GENERATE_BUDGET_MS = 9e5;
var MAX_TIMEOUT_SECONDS = 600;
var MAX_POLL_RESIGNS = 2;
var VideoClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts6.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL5;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT5;
  }
  /**
   * Generate a short video clip from a text prompt (or text + image).
   *
   * Submits an async job, then polls until the video is ready. Typical total
   * wall-time is 60-180s, but upstream status can lag several minutes behind
   * actual completion. If upstream runs past the budget (default 15min),
   * throws with the existing poll URL; check billing before submitting again.
   *
   * @param prompt - Text description of the video
   * @param options - Optional generation parameters
   */
  async generate(prompt, options) {
    if (options?.imageUrl && options?.realFaceAssetId) {
      throw new Error(
        "imageUrl and realFaceAssetId are mutually exclusive; pass at most one."
      );
    }
    if (options?.realFaceAssetId && !/^ta_[A-Za-z0-9]+$/.test(options.realFaceAssetId)) {
      throw new Error(
        "realFaceAssetId must be a Token360 asset id matching 'ta_[A-Za-z0-9]+' (e.g. 'ta_abc123xyz')"
      );
    }
    if (options?.lastFrameUrl && !options?.imageUrl) {
      throw new Error(
        "lastFrameUrl requires imageUrl: imageUrl seeds the FIRST frame and lastFrameUrl the FINAL frame \u2014 send both."
      );
    }
    if (options?.lastFrameUrl && options?.realFaceAssetId) {
      throw new Error(
        "lastFrameUrl and realFaceAssetId are mutually exclusive; first-and-last-frame uses imageUrl + lastFrameUrl."
      );
    }
    if (options?.referenceImageUrls?.length) {
      if (options.imageUrl || options.lastFrameUrl || options.realFaceAssetId) {
        throw new Error(
          "referenceImageUrls is mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId."
        );
      }
      const imageLimit = options.model?.replace(/^bytedance\//, "") === "seedance-2.5" ? 30 : 9;
      if (options.referenceImageUrls.length > imageLimit) {
        throw new Error(`referenceImageUrls accepts at most ${imageLimit} images.`);
      }
    }
    for (const [field, clips] of [
      ["referenceVideos", options?.referenceVideos],
      ["referenceAudios", options?.referenceAudios]
    ]) {
      if (clips === void 0) continue;
      if (clips.length < 1 || clips.length > 3) {
        throw new Error(`${field} accepts 1 to 3 clips.`);
      }
      for (const clip of clips) {
        if (typeof clip?.url !== "string" || !/^https?:\/\//i.test(clip.url)) {
          throw new Error(`${field} URLs must be http(s).`);
        }
        if (clip.role !== void 0 && clip.role !== "reference") {
          throw new Error(`${field} role must be "reference" (or omitted).`);
        }
      }
    }
    if (options?.referenceVideos?.length || options?.referenceAudios?.length) {
      if (options.imageUrl || options.lastFrameUrl || options.realFaceAssetId) {
        throw new Error(
          "referenceVideos / referenceAudios are mutually exclusive with imageUrl, lastFrameUrl, and realFaceAssetId; combine them with referenceImageUrls instead."
        );
      }
      if (options.referenceAudios?.length && !options.referenceImageUrls?.length && !options.referenceVideos?.length) {
        throw new Error("referenceAudios requires referenceImageUrls or referenceVideos.");
      }
    }
    const body = {
      model: options?.model || DEFAULT_MODEL4,
      prompt
    };
    if (options?.imageUrl) body.image_url = options.imageUrl;
    if (options?.lastFrameUrl) body.last_frame_url = options.lastFrameUrl;
    if (options?.referenceImageUrls?.length) body.reference_image_urls = options.referenceImageUrls;
    if (options?.realFaceAssetId) body.real_face_asset_id = options.realFaceAssetId;
    if (options?.durationSeconds !== void 0) body.duration_seconds = options.durationSeconds;
    if (options?.aspectRatio) body.aspect_ratio = options.aspectRatio;
    if (options?.resolution) body.resolution = options.resolution;
    if (options?.generateAudio !== void 0) body.generate_audio = options.generateAudio;
    if (options?.seed !== void 0) body.seed = options.seed;
    if (options?.watermark !== void 0) body.watermark = options.watermark;
    if (options?.returnLastFrame !== void 0) body.return_last_frame = options.returnLastFrame;
    if (options?.referenceVideos !== void 0) body.reference_videos = options.referenceVideos;
    if (options?.referenceAudios !== void 0) body.reference_audios = options.referenceAudios;
    if (options?.bitrateMode !== void 0) body.bitrate_mode = options.bitrateMode;
    if (options?.outputFormat !== void 0) body.output_format = options.outputFormat;
    if (options?.cameraFixed !== void 0) body.camera_fixed = options.cameraFixed;
    if (options?.safetyIdentifier !== void 0) body.safety_identifier = options.safetyIdentifier;
    if (options?.inputType !== void 0) body.input_type = options.inputType;
    const budgetMs = options?.budgetMs ?? DEFAULT_GENERATE_BUDGET_MS;
    return this.submitAndPoll(body, budgetMs);
  }
  /**
   * Generate a video from a standard Seedance `content[]` body.
   *
   * Targets the gateway's `POST /v1/videos` endpoint, which accepts the
   * mainstream multimodal `content` array used by other Seedance APIs. Items
   * may carry a `role` — `first_frame`, `last_frame`, `reference_image`,
   * `reference_video`, `reference_audio` — which the gateway maps to the same
   * validated fields as {@link generate}; a role-less single image keeps its
   * first-frame meaning — so callers already holding a
   * `content[]`-shaped request can submit it unchanged. The gateway validates
   * unsupported inputs *before* charging, then delegates to the same x402
   * submit+poll pipeline as {@link generate}.
   *
   * Most SDK users should prefer {@link generate} (structured options like
   * `imageUrl` / `lastFrameUrl`) — this exists for migrating existing
   * `content[]` payloads with no reshaping.
   *
   * @param content - The Seedance `content` array, e.g.
   *   `[{ type: "text", text: "a red apple spinning" }]` or a text item plus
   *   `{ type: "image_url", image_url: { url: "https://..." } }`.
   * @param options - `model`, `budgetMs`, plus the same camelCase render options
   *   as {@link generate} (`durationSeconds`, `aspectRatio`, `resolution`,
   *   `generateAudio`, `seed`, `watermark`, `returnLastFrame`, `bitrateMode`,
   *   `outputFormat`, `cameraFixed`, `safetyIdentifier`, `inputType`). These are mapped
   *   to the gateway's snake_case fields for you. Any other keys you pass are
   *   forwarded verbatim (use snake_case for those, since the gateway reads
   *   snake_case only).
   */
  async generateFromContent(content, options) {
    if (!Array.isArray(content) || content.length === 0) {
      throw new Error("content must be a non-empty array of Seedance content items.");
    }
    const {
      model,
      budgetMs,
      durationSeconds,
      aspectRatio,
      resolution,
      generateAudio,
      seed,
      watermark,
      returnLastFrame,
      bitrateMode,
      outputFormat,
      cameraFixed,
      safetyIdentifier,
      inputType,
      ...extra
    } = options ?? {};
    const body = { ...extra, content };
    if (model !== void 0) body.model = model;
    if (durationSeconds !== void 0) body.duration_seconds = durationSeconds;
    if (aspectRatio !== void 0) body.aspect_ratio = aspectRatio;
    if (resolution !== void 0) body.resolution = resolution;
    if (generateAudio !== void 0) body.generate_audio = generateAudio;
    if (seed !== void 0) body.seed = seed;
    if (watermark !== void 0) body.watermark = watermark;
    if (returnLastFrame !== void 0) body.return_last_frame = returnLastFrame;
    if (bitrateMode !== void 0) body.bitrate_mode = bitrateMode;
    if (outputFormat !== void 0) body.output_format = outputFormat;
    if (cameraFixed !== void 0) body.camera_fixed = cameraFixed;
    if (safetyIdentifier !== void 0) body.safety_identifier = safetyIdentifier;
    if (inputType !== void 0) body.input_type = inputType;
    return this.submitAndPoll(body, budgetMs ?? DEFAULT_GENERATE_BUDGET_MS, "/v1/videos");
  }
  // --------------------------------------------------------------------
  // Internal: async submit + poll
  // --------------------------------------------------------------------
  async submitAndPoll(body, budgetMs, submitPath = "/v1/videos/generations") {
    const submitUrl = `${this.apiUrl}${submitPath}`;
    const resp402 = await this.fetchWithTimeout(submitUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (this.apiAuth) return this.apiAuth.poll(resp402, budgetMs, POLL_INTERVAL_MS2);
    if (resp402.status !== 402) {
      await this.throwApiError(resp402, "Expected 402 on first POST");
    }
    let { payload: paymentPayload, details } = await this.signFromChallenge(
      resp402,
      submitUrl
    );
    const submitResp = await this.fetchWithTimeout(submitUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (submitResp.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (submitResp.status !== 200 && submitResp.status !== 202) {
      await this.throwApiError(submitResp, "Submit failed");
    }
    const submitData = await submitResp.json();
    if (!submitData.id || !submitData.poll_url) {
      throw new APIError(
        "Submit response missing id/poll_url",
        submitResp.status,
        { response: submitData }
      );
    }
    const pollUrl = this.absolute(submitData.poll_url);
    const deadline = Date.now() + budgetMs;
    let lastStatus = submitData.status || "queued";
    let resignsLeft = MAX_POLL_RESIGNS;
    while (Date.now() < deadline) {
      await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
      const pollResp = await this.fetchWithTimeout(pollUrl, {
        method: "GET",
        headers: { "PAYMENT-SIGNATURE": paymentPayload }
      });
      let pollData = {};
      try {
        pollData = await pollResp.json();
      } catch {
      }
      lastStatus = pollData.status || lastStatus;
      if (pollResp.status === 202 && (lastStatus === "queued" || lastStatus === "in_progress")) {
        continue;
      }
      if (lastStatus === "failed") {
        throw new APIError(
          `Upstream generation failed: ${pollData.error || "unknown"}`,
          pollResp.status,
          sanitizeErrorResponse(pollData)
        );
      }
      if (lastStatus === "completed") {
        const data = pollData;
        if (!settlementSkipped(pollResp)) {
          this.sessionCalls++;
          this.sessionTotalUsd += Number(details.amount) / 1e6;
        }
        const txHash = pollResp.headers.get("x-payment-receipt") || pollResp.headers.get("X-Payment-Receipt");
        if (txHash) data.txHash = txHash;
        return data;
      }
      if (pollResp.status === 402) {
        if (resignsLeft > 0) {
          resignsLeft--;
          const challenge = await this.fetchWithTimeout(pollUrl, { method: "GET" });
          if (challenge.status === 402) {
            ({ payload: paymentPayload, details } = await this.signFromChallenge(
              challenge,
              pollUrl
            ));
            continue;
          }
        }
        throw new PaymentError(
          "Payment verification failed mid-poll (not a signature-expiry). Check the wallet balance and that you poll from the wallet that submitted the job."
        );
      }
      if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
        await this.throwApiError(pollResp, "Poll failed");
      }
    }
    throw new APIError(
      `Video generation did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). A polling timeout does not confirm billing status. Resume the existing poll_url using the same account or wallet; check account Activity or wallet receipts before submitting another job.`,
      504,
      { id: submitData.id, last_status: lastStatus, poll_url: pollUrl }
    );
  }
  /**
   * Parse an x402 challenge response and sign a payment payload for it.
   * Used for the initial submit AND for mid-poll re-signing after the 600s
   * authorization window lapses on long polls.
   */
  async signFromChallenge(resp402, fallbackUrl) {
    const paymentRequired = await this.extractPaymentRequired(resp402);
    const details = extractPaymentDetails(paymentRequired);
    const payload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || fallbackUrl,
        resourceDescription: details.resource?.description || "BlockRun Video Generation",
        // Ensure signed auth covers as much of the polling window as allowed.
        maxTimeoutSeconds: Math.max(details.maxTimeoutSeconds || 0, MAX_TIMEOUT_SECONDS),
        extra: details.extra
      }
    );
    return { payload, details };
  }
  absolute(url) {
    if (url.startsWith("http://") || url.startsWith("https://")) return url;
    const base = this.apiUrl.endsWith("/api") ? this.apiUrl.slice(0, -"/api".length) : this.apiUrl;
    return `${base}${url}`;
  }
  async extractPaymentRequired(resp) {
    const header = resp.headers.get("payment-required");
    if (header) return parsePaymentRequired(header);
    try {
      const body = await resp.json();
      if (body && (body.x402Version !== void 0 || body.accepts !== void 0)) {
        return body;
      }
    } catch {
    }
    throw new PaymentError("402 response but no payment requirements found");
  }
  async throwApiError(resp, prefix) {
    let errorBody;
    try {
      errorBody = await resp.json();
    } catch {
      errorBody = { error: "Request failed" };
    }
    throw new APIError(
      `${prefix}: HTTP ${resp.status}`,
      resp.status,
      sanitizeErrorResponse(errorBody)
    );
  }
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};

// src/voice.ts
var import_accounts7 = require("viem/accounts");
var DEFAULT_API_URL6 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT6 = 6e4;
var CALL_PRICE_USD = 0.54;
var VALID_MODELS = /* @__PURE__ */ new Set(["base", "enhanced", "turbo"]);
var VoiceClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts7.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL6;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT6;
  }
  /**
   * Initiate an AI-powered outbound phone call.
   *
   * Pricing: per call. Returns immediately once the call is queued —
   * poll getStatus() for transcript and recording.
   *
   * @example
   *   const r = await client.call({
   *     to: '+14155552671',
   *     task: 'Confirm the user wants to reschedule to Tuesday 2pm.',
   *     voice: 'maya',
   *     max_duration: 3,
   *   });
   */
  async call(options) {
    if (!options.to || !options.to.trim()) {
      throw new Error("'to' phone number is required (E.164 format)");
    }
    const task = options.task?.trim() ?? "";
    if (task.length < 10) {
      throw new Error("'task' must be at least 10 characters");
    }
    if (task.length > 4e3) {
      throw new Error("'task' must be at most 4000 characters");
    }
    const maxDuration = options.max_duration ?? 5;
    if (maxDuration < 1 || maxDuration > 30) {
      throw new Error("max_duration must be between 1 and 30 minutes");
    }
    if (options.model && !VALID_MODELS.has(options.model)) {
      throw new Error("model must be 'base' | 'enhanced' | 'turbo'");
    }
    if (options.interruption_threshold !== void 0 && (options.interruption_threshold < 50 || options.interruption_threshold > 500)) {
      throw new Error("interruption_threshold must be between 50 and 500");
    }
    const body = {
      to: options.to.trim(),
      task,
      max_duration: maxDuration,
      language: options.language ?? "en-US"
    };
    if (options.from) body.from = options.from.trim();
    if (options.voice) body.voice = options.voice;
    if (options.first_sentence) body.first_sentence = options.first_sentence.trim();
    if (options.wait_for_greeting !== void 0) body.wait_for_greeting = options.wait_for_greeting;
    if (options.interruption_threshold !== void 0) body.interruption_threshold = options.interruption_threshold;
    if (options.model) body.model = options.model;
    return this.requestWithPayment("/v1/voice/call", body);
  }
  /**
   * Poll the status of an in-progress or completed call. Free — no payment.
   *
   * Returns Bland.ai's full call record: status, transcript, recording URL, etc.
   * Most fields populate only once the call ends.
   */
  async getStatus(callId) {
    if (!callId || !callId.trim()) {
      throw new Error("callId is required");
    }
    const url = `${this.apiUrl}/v1/voice/call/${encodeURIComponent(callId.trim())}`;
    const response = await this.fetchWithTimeout(url, {
      method: "GET",
      headers: { Accept: "application/json" }
    });
    if (response.status === 404) {
      throw new APIError(`Call not found: ${callId}`, 404, { call_id: callId });
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    return response.json();
  }
  async requestWithPayment(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    return response.json();
  }
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || `${this.apiUrl}/v1/voice/call`,
        resourceDescription: details.resource?.description || "BlockRun Voice Call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
    }
    const data = await retryResponse.json();
    if (!settlementSkipped(retryResponse)) {
      this.sessionCalls++;
      this.sessionTotalUsd += CALL_PRICE_USD;
    }
    const txHash = retryResponse.headers.get("x-payment-receipt") || retryResponse.headers.get("X-Payment-Receipt");
    if (txHash) data.txHash = txHash;
    return data;
  }
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};

// src/phone.ts
var import_accounts8 = require("viem/accounts");
var DEFAULT_API_URL7 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT7 = 6e4;
var PHONE_PRICES = Object.freeze({
  lookup: 0.01,
  "lookup/fraud": 0.05,
  "numbers/buy": 5,
  "numbers/renew": 5,
  "numbers/list": 1e-3,
  "numbers/release": 0
});
function requireE164(value) {
  if (!value || typeof value !== "string") {
    throw new Error("phoneNumber is required (E.164 format, e.g. '+14155552671')");
  }
  const v = value.trim();
  if (!/^\+\d{7,15}$/.test(v)) {
    throw new Error(
      `phoneNumber must be E.164 (e.g. '+14155552671'), got ${JSON.stringify(value)}`
    );
  }
  return v;
}
var PhoneClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts8.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL7;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT7;
  }
  /** EVM wallet address used for payments. */
  getWalletAddress() {
    return this.account.address;
  }
  /** Session-cumulative USD spend + call count across this client instance. */
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
  // ─── Lookup ────────────────────────────────────────────────────────────
  /**
   * Carrier + line-type lookup.
   *
   * @param phoneNumber E.164 number (e.g. "+14155552671").
   */
  async lookup(phoneNumber) {
    const num = requireE164(phoneNumber);
    return this.request("lookup", { phoneNumber: num });
  }
  /**
   * Lookup + fraud signals (SIM swap, call forwarding).
   *
   * @param phoneNumber E.164 number.
   */
  async lookupFraud(phoneNumber) {
    const num = requireE164(phoneNumber);
    return this.request("lookup/fraud", { phoneNumber: num });
  }
  // ─── Provisioning ──────────────────────────────────────────────────────
  /**
   * Provision a dedicated phone number for 30 days.
   *
   * Payment is settled only after Twilio confirms the purchase, so failed
   * buys never charge your wallet.
   *
   * @param options.country  ISO country code, "US" or "CA" (default "US").
   * @param options.areaCode Optional 3-digit area-code hint. Availability is
   *   not guaranteed — the backend falls back to any number in the country if
   *   the area code can't be matched.
   */
  async buyNumber(options = {}) {
    const country = options.country ?? "US";
    if (country !== "US" && country !== "CA") {
      throw new Error("country must be 'US' or 'CA'");
    }
    const body = { country };
    if (options.areaCode !== void 0) {
      const ac = options.areaCode;
      if (typeof ac !== "string" || !/^\d{3}$/.test(ac)) {
        throw new Error("areaCode must be a 3-digit string, e.g. '415'");
      }
      body.areaCode = ac;
    }
    const data = await this.request("numbers/buy", body);
    return data;
  }
  /**
   * Extend an existing provisioned number by 30 days.
   *
   * @throws {APIError} 403 when the wallet doesn't own the number or it has expired.
   */
  async renewNumber(phoneNumber) {
    const num = requireE164(phoneNumber);
    const data = await this.request("numbers/renew", {
      phoneNumber: num
    });
    return data;
  }
  /** List the wallet's active phone numbers. */
  async listNumbers() {
    const data = await this.request("numbers/list", {});
    return data;
  }
  /**
   * Release a provisioned number back to the Twilio pool. Free, but still
   * flows through x402 so the backend can verify ownership.
   */
  async releaseNumber(phoneNumber) {
    const num = requireE164(phoneNumber);
    const data = await this.request("numbers/release", {
      phoneNumber: num
    });
    return data;
  }
  // ─── Internals ─────────────────────────────────────────────────────────
  async request(path7, body) {
    const url = `${this.apiUrl}/v1/phone/${path7}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    return this.unwrap(response, false);
  }
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const rb = await response.json();
        if (rb.x402 || rb.accepts) paymentHeader = btoa(JSON.stringify(rb));
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || "BlockRun Phone",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retry = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retry.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    const data = await this.unwrap(retry, true);
    if (!settlementSkipped(retry)) {
      this.sessionCalls += 1;
      this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
    }
    const txHash = retry.headers.get("x-payment-receipt") || retry.headers.get("X-Payment-Receipt");
    if (txHash && data && typeof data === "object" && !("txHash" in data)) {
      data.txHash = txHash;
    }
    return data;
  }
  async unwrap(response, afterPayment) {
    if (response.ok) {
      return response.json();
    }
    let errorBody;
    try {
      errorBody = await response.json();
    } catch {
      errorBody = { error: "Request failed" };
    }
    const prefix = afterPayment ? "API error after payment" : "API error";
    throw new APIError(
      `${prefix}: ${response.status}`,
      response.status,
      sanitizeErrorResponse(errorBody)
    );
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
};

// src/portrait.ts
var import_accounts9 = require("viem/accounts");
var DEFAULT_API_URL8 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT8 = 6e4;
var PORTRAIT_ENROLLMENT_PRICE_USD = 0.01;
var PortraitClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts9.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL8;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT8;
  }
  /** EVM wallet address used for payments. */
  getWalletAddress() {
    return this.account.address;
  }
  /** Session-cumulative USD spend + call count across this client instance. */
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
  /**
   * Enroll a Virtual Portrait from a face image URL. (promo).
   *
   * Payment is settled only after Token360 confirms the enrollment, so failed
   * enrollments never charge your wallet.
   *
   * @param options.name     Display name (1–64 chars).
   * @param options.imageUrl Public HTTPS URL to a JPG/PNG/WEBP image (≤10 MB).
   * @returns The enrollment record, including `asset_id` (`ta_xxxxxx`).
   */
  async enroll(options) {
    const name = typeof options?.name === "string" ? options.name.trim() : "";
    if (name.length < 1 || name.length > 64) {
      throw new Error("name is required and must be 1\u201364 characters");
    }
    const imageUrl = typeof options?.imageUrl === "string" ? options.imageUrl.trim() : "";
    if (!/^https?:\/\//i.test(imageUrl)) {
      throw new Error(
        "imageUrl must be a public http(s) URL to a JPG/PNG/WEBP image"
      );
    }
    const data = await this.request({
      name,
      image_url: imageUrl
    });
    return data;
  }
  // ─── Internals ─────────────────────────────────────────────────────────
  async request(body) {
    const url = `${this.apiUrl}/v1/portrait/enroll`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    return this.unwrap(response, false);
  }
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const rb = await response.json();
        if (rb.x402 || rb.accepts) paymentHeader = btoa(JSON.stringify(rb));
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || "BlockRun Virtual Portrait",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retry = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retry.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    const data = await this.unwrap(retry, true);
    if (!settlementSkipped(retry)) {
      this.sessionCalls += 1;
      this.sessionTotalUsd += Number.parseFloat(details.amount) / 1e6;
    }
    if (data && typeof data === "object" && !("txHash" in data)) {
      const settlement = data.settlement;
      if (settlement?.tx_hash) {
        data.txHash = settlement.tx_hash;
      }
    }
    return data;
  }
  async unwrap(response, afterPayment) {
    if (response.ok) {
      return response.json();
    }
    let errorBody;
    try {
      errorBody = await response.json();
    } catch {
      errorBody = { error: "Request failed" };
    }
    const prefix = afterPayment ? "API error after payment" : "API error";
    throw new APIError(
      `${prefix}: ${response.status}`,
      response.status,
      sanitizeErrorResponse(errorBody)
    );
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
};

// src/search.ts
var import_accounts10 = require("viem/accounts");
var DEFAULT_API_URL9 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT9 = 6e4;
var SearchClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts10.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL9;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT9;
  }
  async search(query, options) {
    if (!query || query.length > 1e3) {
      throw new Error("query must be 1-1000 characters");
    }
    const maxResults = options?.maxResults ?? 10;
    if (maxResults < 1 || maxResults > 50) {
      throw new Error("maxResults must be between 1 and 50");
    }
    const body = {
      query,
      max_results: maxResults
    };
    if (options?.sources) body.sources = options.sources;
    if (options?.fromDate) body.from_date = options.fromDate;
    if (options?.toDate) body.to_date = options.toDate;
    return this.requestWithPayment("/v1/search", body);
  }
  async requestWithPayment(endpoint, body) {
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return response.json();
  }
  async handlePaymentAndRetry(url, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || "BlockRun Search",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retry = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retry.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retry.ok) {
      let errorBody;
      try {
        errorBody = await retry.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retry.status}`,
        retry.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return retry.json();
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
};

// src/price.ts
var import_accounts11 = require("viem/accounts");
var DEFAULT_API_URL10 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT10 = 3e4;
var PriceClient = class {
  apiAuth;
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  account = null;
  privateKey = null;
  apiUrl;
  timeout;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
    const privateKey = this.apiAuth ? void 0 : options.privateKey || envKey;
    const requireWallet2 = options.requireWallet ?? true;
    if (!this.apiAuth && !privateKey && requireWallet2) {
      throw new Error(
        "Private key required for paid endpoints. Pass privateKey in options, set BLOCKRUN_WALLET_KEY, or pass requireWallet: false for free-only usage."
      );
    }
    if (privateKey) {
      validatePrivateKey(privateKey);
      this.privateKey = privateKey;
      this.account = (0, import_accounts11.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL10;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT10;
  }
  async price(category, symbol, options) {
    if (!symbol) throw new Error("symbol is required");
    const path7 = categoryPath(category, options?.market, "price", symbol);
    const query = {};
    if (options?.session) query.session = options.session;
    const data = await this.getWithPayment(path7, query);
    return {
      symbol: data.symbol ?? symbol.toUpperCase(),
      price: data.price,
      publishTime: data.publishTime,
      confidence: data.confidence,
      feedId: data.feedId,
      timestamp: data.timestamp,
      assetType: data.assetType,
      category: data.category,
      source: data.source,
      free: data.free
    };
  }
  async history(category, symbol, options) {
    if (!symbol) throw new Error("symbol is required");
    if (!options.from || options.from <= 0) {
      throw new Error("history requires options.from (unix seconds)");
    }
    const path7 = categoryPath(category, options.market, "history", symbol);
    const query = {
      resolution: options.resolution ?? "D",
      from: String(options.from)
    };
    if (options.to) query.to = String(options.to);
    if (options.session) query.session = options.session;
    const data = await this.getWithPayment(path7, query);
    return {
      symbol: data.symbol ?? symbol.toUpperCase(),
      resolution: data.resolution ?? (options.resolution ?? "D"),
      from: data.from,
      to: data.to,
      bars: data.bars ?? [],
      source: data.source,
      category: data.category
    };
  }
  async listSymbols(category, options) {
    const path7 = categoryPath(category, options?.market, "list");
    const query = {
      limit: String(options?.limit && options.limit > 0 ? options.limit : 100)
    };
    if (options?.query) query.q = options.query;
    const data = await this.getWithPayment(path7, query);
    if (Array.isArray(data)) {
      return { symbols: data, count: data.length };
    }
    const obj = data;
    const symbols = obj.symbols ?? obj.feeds ?? [];
    return {
      symbols,
      count: obj.count ?? symbols.length
    };
  }
  getWalletAddress() {
    return this.account?.address ?? null;
  }
  async getWithPayment(endpoint, query) {
    const url = buildUrl(`${this.apiUrl}${endpoint}`, query);
    const response = await this.fetchWithTimeout(url, { method: "GET" });
    if (response.status === 402) {
      if (!this.privateKey || !this.account) {
        throw new PaymentError(
          `${endpoint} returned 402 Payment Required but no wallet is configured.`
        );
      }
      return this.payAndRetry(url, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error: ${response.status}`,
        response.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return response.json();
  }
  async payAndRetry(url, response) {
    if (!this.privateKey || !this.account) {
      throw new PaymentError("Wallet required to sign payment.");
    }
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402) {
          paymentHeader = btoa(JSON.stringify(respBody.x402));
        } else if (respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || "BlockRun Price Data",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retry = await this.fetchWithTimeout(url, {
      method: "GET",
      headers: { "PAYMENT-SIGNATURE": paymentPayload }
    });
    if (retry.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retry.ok) {
      let errorBody;
      try {
        errorBody = await retry.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(
        `API error after payment: ${retry.status}`,
        retry.status,
        sanitizeErrorResponse(errorBody)
      );
    }
    return retry.json();
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
};
function categoryPath(category, market, kind, symbol) {
  let base;
  if (category === "stocks") {
    if (!market) {
      throw new Error("market is required when category === 'stocks'");
    }
    base = `/v1/stocks/${market}`;
  } else if (["crypto", "fx", "commodity", "usstock"].includes(category)) {
    base = `/v1/${category}`;
  } else {
    throw new Error(`unknown category: ${category}`);
  }
  if (!symbol) return `${base}/${kind}`;
  return `${base}/${kind}/${encodeURIComponent(symbol.toUpperCase())}`;
}
function buildUrl(base, query) {
  const params = Object.entries(query);
  if (params.length === 0) return base;
  const qs = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
  return `${base}?${qs}`;
}

// src/surf.ts
var import_accounts12 = require("viem/accounts");
var DEFAULT_API_URL11 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT11 = 6e4;
var SURF_PREFIX = "/v1/surf";
var warnedRetired = false;
var SurfClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  constructor(options = {}) {
    if (!warnedRetired) {
      warnedRetired = true;
      console.warn(
        "[blockrun] SurfClient is deprecated: the gateway retired /v1/surf/* (410 endpoint_retired). Use PriceClient, LLMClient.defi*, LLMClient.pm() or LLMClient.dex* instead."
      );
    }
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts12.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL11;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT11;
  }
  /**
   * GET a Surf endpoint. `path` is everything after `/v1/surf` (a leading
   * `/v1/surf` is tolerated and stripped). Query params are URL-encoded;
   * arrays become repeated keys (`?a=1&a=2`).
   */
  async get(path7, params) {
    const url = this.buildUrl(path7, params);
    return this.requestWithPayment(url, "GET");
  }
  /**
   * POST a Surf endpoint with a JSON body. Same path normalization as `get`.
   */
  async post(path7, body) {
    const url = this.buildUrl(path7);
    return this.requestWithPayment(url, "POST", body);
  }
  buildUrl(path7, params) {
    let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
    if (!normalized.startsWith(SURF_PREFIX)) {
      normalized = `${SURF_PREFIX}${normalized}`;
    }
    const base = `${this.apiUrl}${normalized}`;
    if (!params) return base;
    const qs = new URLSearchParams();
    for (const [key, value] of Object.entries(params)) {
      if (value === void 0 || value === null) continue;
      if (Array.isArray(value)) {
        for (const v of value) {
          if (v === void 0 || v === null) continue;
          qs.append(key, String(v));
        }
      } else {
        qs.append(key, String(value));
      }
    }
    const query = qs.toString();
    return query ? `${base}?${query}` : base;
  }
  async requestWithPayment(url, method, body) {
    const init = { method };
    if (method === "POST") {
      init.headers = { "Content-Type": "application/json" };
      init.body = JSON.stringify(body ?? {});
    }
    const response = await this.fetchWithTimeout(url, init);
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, method, body, response);
    }
    if (!response.ok) {
      await this.throwApiError(response, `Surf request failed (${method} ${url})`);
    }
    return response.json();
  }
  async handlePaymentAndRetry(url, method, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402Version !== void 0 || respBody.accepts !== void 0) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || "BlockRun Surf",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retryInit = {
      method,
      headers: { "PAYMENT-SIGNATURE": paymentPayload }
    };
    if (method === "POST") {
      retryInit.headers = {
        ...retryInit.headers,
        "Content-Type": "application/json"
      };
      retryInit.body = JSON.stringify(body ?? {});
    }
    const retry = await this.fetchWithTimeout(url, retryInit);
    if (retry.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retry.ok) {
      await this.throwApiError(retry, `Surf request failed after payment (${method} ${url})`);
    }
    return retry.json();
  }
  async throwApiError(resp, prefix) {
    let errorBody;
    try {
      errorBody = await resp.json();
    } catch {
      errorBody = { error: "Request failed" };
    }
    throw new APIError(
      `${prefix}: HTTP ${resp.status}`,
      resp.status,
      sanitizeErrorResponse(errorBody)
    );
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
};

// src/rpc.ts
var import_accounts13 = require("viem/accounts");
var DEFAULT_API_URL12 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT12 = 6e4;
var RPC_PRICE_USD = 2e-3;
var SUPPORTED_NETWORKS = [
  // EVM
  "ethereum",
  "base",
  "arbitrum",
  "arbitrum-nova",
  "optimism",
  "polygon",
  "bsc",
  "avalanche",
  "fantom",
  "cronos",
  "celo",
  "gnosis",
  "zksync",
  "berachain",
  "unichain",
  "monad",
  "chiliz",
  "moonbeam",
  "aurora",
  "flare",
  "oasis",
  "kaia",
  "sonic",
  "xdc",
  "abstract",
  "hyperevm",
  "plume",
  "ronin",
  "rootstock",
  // Non-EVM (JSON-RPC-compatible)
  "solana",
  "bitcoin",
  "litecoin",
  "dogecoin",
  "bitcoin-cash",
  "near",
  "sui",
  "ripple",
  "polkadot",
  "kusama",
  "zcash"
];
var NETWORK_ALIASES = {
  eth: "ethereum",
  arb: "arbitrum",
  "arbitrum-one": "arbitrum",
  "arb-one": "arbitrum",
  "arb-nova": "arbitrum-nova",
  op: "optimism",
  matic: "polygon",
  pol: "polygon",
  bnb: "bsc",
  binance: "bsc",
  "binance-smart-chain": "bsc",
  avax: "avalanche",
  ftm: "fantom",
  bera: "berachain",
  klaytn: "kaia",
  chz: "chiliz",
  hyperliquid: "hyperevm",
  rsk: "rootstock",
  sol: "solana",
  btc: "bitcoin",
  ltc: "litecoin",
  doge: "dogecoin",
  bch: "bitcoin-cash",
  xrp: "ripple",
  xrpl: "ripple",
  dot: "polkadot",
  zec: "zcash"
};
var RpcClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts13.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL12;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT12;
  }
  /**
   * Make a single JSON-RPC 2.0 call. Flat per-call rate.
   *
   * @param network - Chain name (e.g. "ethereum", "base", "solana") or a
   *                  common alias ("eth", "sol", "matic", ...). See
   *                  SUPPORTED_NETWORKS / NETWORK_ALIASES.
   * @param method - Chain RPC method, e.g. "eth_blockNumber", "eth_call",
   *                 "eth_getBalance" (EVM) or "getSlot", "getAccountInfo"
   *                 (Solana).
   * @param params - Method-specific params array (optional).
   * @returns RpcResponse with `result` (or JSON-RPC `error`), plus
   *          `network`, `cacheHit` and `txHash` metadata.
   *
   * @example
   * const block = await client.call('ethereum', 'eth_blockNumber');
   * console.log(parseInt(block.result as string, 16));
   */
  async call(network, method, params) {
    const body = { jsonrpc: "2.0", id: 1, method };
    if (params !== void 0) body.params = params;
    const { data, headers } = await this.requestWithPayment(network, body);
    return this.toResponse(data, headers);
  }
  /**
   * Make a JSON-RPC 2.0 batch call. Priced per element (rate x N).
   *
   * @param network - Chain name or alias (see {@link call}).
   * @param requests - Requests, each with a `method` and optional
   *                   `params` / `id`. `jsonrpc` and missing ids are
   *                   filled in automatically.
   * @returns Array of RpcResponse, in upstream order.
   *
   * @example
   * const out = await client.batch('base', [
   *   { method: 'eth_blockNumber' },
   *   { method: 'eth_gasPrice' },
   * ]);
   */
  async batch(network, requests) {
    if (!requests.length) {
      throw new Error("batch requires at least one request");
    }
    const body = requests.map((req, i) => {
      if (!req.method) throw new Error(`batch request ${i} is missing 'method'`);
      return { jsonrpc: "2.0", id: i + 1, ...req };
    });
    const { data, headers } = await this.requestWithPayment(network, body);
    const items = Array.isArray(data) ? data : [data];
    return items.map((item) => this.toResponse(item, headers));
  }
  toResponse(data, headers) {
    const base = data !== null && typeof data === "object" && !Array.isArray(data) ? { ...data } : { result: data };
    const network = headers.get("x-network");
    if (network) base.network = network;
    base.cacheHit = (headers.get("x-cache") || "").toUpperCase() === "HIT";
    const txHash = headers.get("x-payment-receipt") || headers.get("X-Payment-Receipt");
    if (txHash) base.txHash = txHash;
    return base;
  }
  async requestWithPayment(network, body) {
    const endpoint = `/v1/rpc/${network}`;
    const url = `${this.apiUrl}${endpoint}`;
    const response = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body)
    });
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, endpoint, body, response);
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
    }
    return { data: await response.json(), headers: response.headers };
  }
  async handlePaymentAndRetry(url, endpoint, body, response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402 || respBody.accepts) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || `${this.apiUrl}${endpoint}`,
        resourceDescription: details.resource?.description || "BlockRun Multi-chain RPC",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra
      }
    );
    const retryResponse = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body)
    });
    if (retryResponse.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (!retryResponse.ok) {
      let errorBody;
      try {
        errorBody = await retryResponse.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      throw new APIError(`API error after payment: ${retryResponse.status}`, retryResponse.status, sanitizeErrorResponse(errorBody));
    }
    if (!settlementSkipped(retryResponse)) {
      this.sessionCalls++;
      const paidUsd = Number(details.amount) / 1e6;
      if (Number.isFinite(paidUsd)) this.sessionTotalUsd += paidUsd;
    }
    return { data: await retryResponse.json(), headers: retryResponse.headers };
  }
  async fetchWithTimeout(url, options) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};

// src/sse.ts
async function* readSseFrames(response, onMissingBody) {
  if (!response.body) throw onMissingBody(response.status);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() || "";
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed || !trimmed.startsWith("data: ")) continue;
        const data = trimmed.slice(6);
        if (data === "[DONE]") return;
        try {
          yield JSON.parse(data);
        } catch {
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

// src/blockrun.ts
var import_accounts14 = require("viem/accounts");
var DEFAULT_API_URL13 = "https://blockrun.ai/api";
var DEFAULT_TIMEOUT13 = 6e4;
var DEFAULT_POLL_INTERVAL_MS = 5e3;
var DEFAULT_POLL_BUDGET_MS = 3e5;
var MAX_SIGNED_AUTH_SECONDS = 600;
var BlockrunClient = class {
  _account;
  get account() {
    return requireWallet(this._account);
  }
  apiAuth;
  /** Active billing mode. */
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.BLOCKRUN_WALLET_KEY || process.env.BASE_CHAIN_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set BLOCKRUN_WALLET_KEY environment variable."
        );
      }
      validatePrivateKey(privateKey);
      this._privateKey = privateKey;
      this._account = (0, import_accounts14.privateKeyToAccount)(privateKey);
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || DEFAULT_API_URL13;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    this.timeout = options.timeout || DEFAULT_TIMEOUT13;
  }
  /**
   * GET a BlockRun endpoint. `path` is everything after `/api` (a leading
   * `/api` is tolerated and stripped). Query params are URL-encoded; arrays
   * become repeated keys (`?a=1&a=2`); undefined/null are dropped.
   */
  async get(path7, params) {
    const url = this.buildUrl(path7, params);
    return this.requestWithPayment(url, "GET");
  }
  /**
   * POST a BlockRun endpoint with a JSON body.
   */
  async post(path7, body) {
    const url = this.buildUrl(path7);
    return this.requestWithPayment(url, "POST", body);
  }
  /**
   * Submit a long-running job and poll until it completes.
   *
   * Pattern: submit → 402 → sign → 202 `{ id, poll_url, status }` → loop GET
   * the poll_url with the SAME `PAYMENT-SIGNATURE` until status=completed (or
   * deadline exceeded). Settlement happens only when upstream returns 200 +
   * completed — upstream failure or caller giving up = no charge.
   *
   * **EVM only, and enforced upstream.** Replaying one signature works because
   * an EIP-3009 authorization stays valid for as long as it was signed for. A
   * Solana payment is a transaction pinned to a recent blockhash and expires in
   * ~150 blocks (~60s), so replaying one could not cover a long render. No
   * guard is needed here: `signFrom402` already refuses a `solana:` network
   * outright, so this loop is unreachable on Solana. Use `SolanaLLMClient`,
   * whose poll loop signs afresh each time; the gateway binds a job to the
   * payer address rather than to the signature, and still settles exactly once.
   *
   * If the gateway returns 200 directly on submit (no async surface), this
   * short-circuits and returns the body. Most long-running endpoints (image,
   * video, music, voice) return 202 with a poll_url.
   */
  async poll(path7, body, options) {
    const submitUrl = this.buildUrl(path7);
    const budgetMs = options?.budgetMs ?? DEFAULT_POLL_BUDGET_MS;
    const intervalMs = options?.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
    const resp402 = await this.fetchWithTimeout(submitUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body ?? {})
    });
    if (this.apiAuth) return this.apiAuth.poll(resp402, budgetMs, intervalMs);
    if (resp402.status === 200) {
      return resp402.json();
    }
    if (resp402.status !== 402) {
      await this.throwApiError(resp402, `poll submit failed (${submitUrl})`);
    }
    const { paymentPayload, costUsd } = await this.signFrom402(resp402, submitUrl, {
      description: "BlockRun async job",
      maxTimeoutSeconds: MAX_SIGNED_AUTH_SECONDS
    });
    const submitResp = await this.fetchWithTimeout(submitUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "PAYMENT-SIGNATURE": paymentPayload
      },
      body: JSON.stringify(body ?? {})
    });
    if (submitResp.status === 402) {
      throw new PaymentError("Payment was rejected. Check your wallet balance.");
    }
    if (submitResp.status !== 200 && submitResp.status !== 202) {
      await this.throwApiError(submitResp, `poll submit failed (${submitUrl})`);
    }
    if (submitResp.status === 200) {
      this.recordSpending(submitResp, costUsd);
      return submitResp.json();
    }
    const submitData = await submitResp.json();
    if (!submitData.id || !submitData.poll_url) {
      throw new APIError(
        "Async submit response missing id/poll_url",
        submitResp.status,
        { response: submitData }
      );
    }
    const pollUrl = this.absolute(submitData.poll_url);
    const deadline = Date.now() + budgetMs;
    let lastStatus = submitData.status || "queued";
    while (Date.now() < deadline) {
      await sleep(intervalMs);
      const pollResp = await this.fetchWithTimeout(pollUrl, {
        method: "GET",
        headers: { "PAYMENT-SIGNATURE": paymentPayload }
      });
      let pollData = {};
      try {
        pollData = await pollResp.json();
      } catch {
      }
      lastStatus = pollData.status || lastStatus;
      if (pollResp.status === 202 && (lastStatus === "queued" || lastStatus === "in_progress")) {
        continue;
      }
      if (lastStatus === "failed") {
        throw new APIError(
          `Upstream job failed: ${pollData.error || "unknown"}`,
          pollResp.status,
          sanitizeErrorResponse(pollData)
        );
      }
      if (pollResp.status === 200 && lastStatus === "completed") {
        this.recordSpending(pollResp, costUsd);
        return pollData;
      }
      if (pollResp.status !== 200 && pollResp.status !== 202 && pollResp.status !== 504) {
        await this.throwApiError(pollResp, `poll failed (${pollUrl})`);
      }
    }
    throw new APIError(
      `Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). No payment was taken.`,
      504,
      { id: submitData.id, last_status: lastStatus }
    );
  }
  /**
   * Stream a Server-Sent Events endpoint.
   *
   * Yields each `data: …` line parsed as JSON. Stops when the upstream emits
   * `data: [DONE]` or closes the connection. Caller is responsible for typing
   * the chunk shape; pass a generic for typed yields.
   *
   * Example — streaming chat:
   *   for await (const chunk of br.stream<ChatChunk>("/v1/chat/completions", {
   *     model: "anthropic/claude-sonnet-4-6",
   *     messages: [{ role: "user", content: "Hi" }],
   *     stream: true,
   *   })) {
   *     process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
   *   }
   */
  async *stream(path7, body) {
    const url = this.buildUrl(path7);
    const requestBody = JSON.stringify(body ?? {});
    const resp402 = await this.fetchWithTimeout(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: requestBody
    });
    let streamResp;
    if (resp402.status === 200) {
      streamResp = resp402;
    } else if (resp402.status === 402) {
      const { paymentPayload, costUsd } = await this.signFrom402(resp402, url, {
        description: "BlockRun stream"
      });
      streamResp = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "PAYMENT-SIGNATURE": paymentPayload
        },
        body: requestBody
      });
      if (streamResp.status === 402) {
        throw new PaymentError(
          "Payment was rejected. Check your wallet balance."
        );
      }
      if (!streamResp.ok) {
        await this.throwApiError(streamResp, `stream failed after payment (${url})`);
      }
      this.recordSpending(streamResp, costUsd);
    } else {
      await this.throwApiError(resp402, `stream failed (${url})`);
      return;
    }
    yield* readSseFrames(
      streamResp,
      (status2) => new APIError("Stream response has no body", status2, {})
    );
  }
  // --------------------------------------------------------------------
  // Internal: shared infrastructure
  // --------------------------------------------------------------------
  buildUrl(path7, params) {
    let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
    if (normalized.startsWith("/api/")) {
      normalized = normalized.slice(4);
    }
    const base = `${this.apiUrl}${normalized}`;
    if (!params) return base;
    const qs = new URLSearchParams();
    for (const [key, value] of Object.entries(params)) {
      if (value === void 0 || value === null) continue;
      if (Array.isArray(value)) {
        for (const v of value) {
          if (v === void 0 || v === null) continue;
          qs.append(key, String(v));
        }
      } else {
        qs.append(key, String(value));
      }
    }
    const query = qs.toString();
    return query ? `${base}?${query}` : base;
  }
  absolute(url) {
    if (url.startsWith("http://") || url.startsWith("https://")) return url;
    const base = this.apiUrl.endsWith("/api") ? this.apiUrl.slice(0, -"/api".length) : this.apiUrl;
    return `${base}${url}`;
  }
  async requestWithPayment(url, method, body) {
    const init = { method };
    if (method === "POST") {
      init.headers = { "Content-Type": "application/json" };
      init.body = JSON.stringify(body ?? {});
    }
    const response = await this.fetchWithTimeout(url, init);
    if (response.status === 402) {
      return this.handlePaymentAndRetry(url, method, body, response);
    }
    if (!response.ok) {
      await this.throwApiError(response, `${method} ${url} failed`);
    }
    return response.json();
  }
  async handlePaymentAndRetry(url, method, body, response) {
    const { paymentPayload, costUsd } = await this.signFrom402(response, url, {
      description: "BlockRun"
    });
    const retryInit = {
      method,
      headers: { "PAYMENT-SIGNATURE": paymentPayload }
    };
    if (method === "POST") {
      retryInit.headers = {
        ...retryInit.headers,
        "Content-Type": "application/json"
      };
      retryInit.body = JSON.stringify(body ?? {});
    }
    const retry = await this.fetchWithTimeout(url, retryInit);
    if (retry.status === 402) {
      throw new PaymentError(
        "Payment was rejected. Check your wallet balance."
      );
    }
    if (!retry.ok) {
      await this.throwApiError(retry, `${method} ${url} failed after payment`);
    }
    this.recordSpending(retry, costUsd);
    return retry.json();
  }
  /**
   * Read a 402 response's payment requirements (header or body), then sign and
   * return the base64 PAYMENT-SIGNATURE payload with what it pays, in USD.
   * The cost stays with the call that signed it (booked by `recordSpending`):
   * concurrent calls on one client never share it.
   */
  async signFrom402(response, url, opts) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.x402Version !== void 0 || respBody.accepts !== void 0) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    const paymentRequired = parsePaymentRequired(paymentHeader);
    const details = extractPaymentDetails(paymentRequired);
    const costUsd = parseFloat(details.amount) / 1e6;
    const paymentPayload = await createPaymentPayload(
      this.privateKey,
      this.account.address,
      details.recipient,
      details.amount,
      details.network || "eip155:8453",
      {
        resourceUrl: details.resource?.url || url,
        resourceDescription: details.resource?.description || opts.description,
        maxTimeoutSeconds: Math.max(
          details.maxTimeoutSeconds || 0,
          opts.maxTimeoutSeconds || 300
        ),
        extra: details.extra,
        asset: details.asset
      }
    );
    return { paymentPayload, costUsd };
  }
  /**
   * Book `costUsd`, what the call that `response` answered signed. A response
   * whose settlement the gateway skipped ({@link settlementSkipped}:
   * `X-Settlement-Skipped: <reason>` or `X-Payment-Waived`) moved nothing, so
   * nothing is booked. `X-Payment-Settled: false` alone is still booked: the
   * payment is owed.
   */
  recordSpending(response, costUsd) {
    if (settlementSkipped(response)) return;
    if (costUsd > 0) {
      this.sessionCalls += 1;
      this.sessionTotalUsd += costUsd;
    }
  }
  async throwApiError(resp, prefix) {
    let errorBody;
    try {
      errorBody = await resp.json();
    } catch {
      errorBody = { error: "Request failed" };
    }
    throw new APIError(
      `${prefix}: HTTP ${resp.status}`,
      resp.status,
      sanitizeErrorResponse(errorBody)
    );
  }
  async fetchWithTimeout(url, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...init, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
  // --------------------------------------------------------------------
  // Public surface: wallet + spending
  // --------------------------------------------------------------------
  getWalletAddress() {
    return this.account.address;
  }
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
};
function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

// src/wallet.ts
var import_accounts16 = require("viem/accounts");

// node_modules/.pnpm/@blockrun+core@0.1.1_bufferutil@4.1.0_typescript@6.0.3_utf-8-validate@6.0.6_zod@3.25.76/node_modules/@blockrun/core/dist/config.js
var fs2 = __toESM(require("fs"), 1);
var os2 = __toESM(require("os"), 1);
var path2 = __toESM(require("path"), 1);
function homeDir() {
  return process.env.BLOCKRUN_HOME || os2.homedir();
}
function blockrunDir() {
  return path2.join(homeDir(), ".blockrun");
}
function paths() {
  const dir = blockrunDir();
  return {
    dir,
    session: path2.join(dir, ".session"),
    legacy: path2.join(dir, "wallet.key"),
    solana: path2.join(dir, ".solana"),
    config: path2.join(dir, "config.json"),
    costLog: path2.join(dir, "cost_log.jsonl")
  };
}

// node_modules/.pnpm/@blockrun+core@0.1.1_bufferutil@4.1.0_typescript@6.0.3_utf-8-validate@6.0.6_zod@3.25.76/node_modules/@blockrun/core/dist/wallet.js
var fs3 = __toESM(require("fs"), 1);
var path3 = __toESM(require("path"), 1);
var import_accounts15 = require("viem/accounts");
function normalize(raw) {
  const k = raw.trim();
  return k.startsWith("0x") ? k : `0x${k}`;
}
function addressFromKey(raw) {
  try {
    return (0, import_accounts15.privateKeyToAccount)(normalize(raw)).address;
  } catch {
    return null;
  }
}
function scanWallets() {
  const home = homeDir();
  const results = [];
  try {
    for (const entry of fs3.readdirSync(home, { withFileTypes: true })) {
      if (!entry.name.startsWith(".") || !entry.isDirectory())
        continue;
      const walletFile2 = path3.join(home, entry.name, "wallet.json");
      if (!fs3.existsSync(walletFile2))
        continue;
      try {
        const data = JSON.parse(fs3.readFileSync(walletFile2, "utf-8"));
        const pk = data.privateKey || "";
        if (!pk)
          continue;
        const derived = addressFromKey(pk);
        if (!derived)
          continue;
        results.push({ mtime: fs3.statSync(walletFile2).mtimeMs, privateKey: pk, address: derived, source: walletFile2 });
      } catch {
        continue;
      }
    }
  } catch {
  }
  results.sort((a, b) => b.mtime - a.mtime);
  return results.map(({ privateKey, address, source }) => ({ privateKey, address, source }));
}
function listDiscoveredWallets() {
  return scanWallets().map(({ address, source }) => ({ address, source }));
}
function adoptWallet(address) {
  const wanted = address.trim().toLowerCase();
  const p = paths();
  for (const entry of scanWallets()) {
    if (entry.address.toLowerCase() !== wanted)
      continue;
    const privateKey = normalize(entry.privateKey);
    if (fs3.existsSync(p.session)) {
      const current = fs3.readFileSync(p.session, "utf8").trim();
      if (current && normalize(current) !== privateKey) {
        const backup = path3.join(p.dir, `.session.backup-${Math.floor(Date.now() / 1e3)}`);
        fs3.writeFileSync(backup, current, { mode: 384 });
      }
    }
    fs3.mkdirSync(p.dir, { recursive: true });
    fs3.writeFileSync(p.session, privateKey, { mode: 384 });
    return { address: entry.address, privateKey, source: "session" };
  }
  const available = listDiscoveredWallets().map((w) => w.address);
  throw new Error(`No discovered wallet controls ${address}. Available: ${available.length ? available.join(", ") : "none"}`);
}
function resolveFromFiles() {
  const p = paths();
  if (fs3.existsSync(p.session)) {
    const raw = fs3.readFileSync(p.session, "utf8").trim();
    if (raw)
      return { privateKey: normalize(raw), source: "session" };
  }
  if (fs3.existsSync(p.legacy)) {
    const raw = fs3.readFileSync(p.legacy, "utf8").trim();
    if (raw)
      return { privateKey: normalize(raw), source: "legacy" };
  }
  return null;
}
function resolvePrivateKey(env = process.env) {
  const fromEnv = env.BLOCKRUN_WALLET_KEY || env.BASE_CHAIN_WALLET_KEY;
  if (fromEnv && fromEnv.trim()) {
    return { privateKey: normalize(fromEnv), source: "env" };
  }
  return resolveFromFiles();
}

// src/wallet.ts
var fs4 = __toESM(require("fs"), 1);
var USDC_BASE_CONTRACT = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
var BASE_CHAIN_ID2 = "8453";
function walletDir() {
  return paths().dir;
}
function walletFile() {
  return paths().session;
}
function createWallet() {
  const privateKey = (0, import_accounts16.generatePrivateKey)();
  const account = (0, import_accounts16.privateKeyToAccount)(privateKey);
  return {
    address: account.address,
    privateKey
  };
}
function saveWallet(privateKey) {
  const dir = walletDir();
  const file = walletFile();
  if (!fs4.existsSync(dir)) {
    fs4.mkdirSync(dir, { recursive: true });
  }
  fs4.writeFileSync(file, privateKey, { mode: 384 });
  return file;
}
function scanWallets2() {
  return scanWallets();
}
function listDiscoveredWallets2() {
  return listDiscoveredWallets();
}
function importWallet(address) {
  return adoptWallet(address).address;
}
function loadWallet() {
  return resolveFromFiles()?.privateKey ?? null;
}
function formatWalletMigrationNotice(newAddress) {
  let addresses;
  try {
    addresses = listDiscoveredWallets().map((w) => w.address);
  } catch {
    return null;
  }
  if (addresses.length === 0) return null;
  const found = addresses.map((addr) => `  ${addr}`).join("\n");
  return `
NOTICE: BlockRun created a new wallet, but also found existing wallet(s)
belonging to other applications on this system:

${found}

BlockRun now uses only its own wallet:

  ${newAddress}

Discovered wallets are never adopted automatically \u2014 one may belong to a
different application, or have been planted to make you fund an address you
do not control.

If an address above is yours and holds your USDC, adopt it deliberately:

  import { importWallet } from '@blockrun/llm';
  importWallet("<address-from-the-list-above>");

Your current wallet is backed up first. You can also set
BLOCKRUN_WALLET_KEY=<private-key> for a single run without changing anything.
`;
}
function getOrCreateWallet() {
  const resolved = resolvePrivateKey();
  if (resolved) {
    const account = (0, import_accounts16.privateKeyToAccount)(resolved.privateKey);
    return { address: account.address, privateKey: resolved.privateKey, isNew: false };
  }
  const { address, privateKey } = createWallet();
  saveWallet(privateKey);
  return { address, privateKey, isNew: true };
}
function getWalletAddress() {
  const resolved = resolvePrivateKey();
  return resolved ? (0, import_accounts16.privateKeyToAccount)(resolved.privateKey).address : null;
}
function getEip681Uri(address, amountUsdc = 1) {
  const amountWei = Math.floor(amountUsdc * 1e6);
  return `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}&uint256=${amountWei}`;
}
function getPaymentLinks(address) {
  return {
    basescan: `https://basescan.org/address/${address}`,
    walletLink: `ethereum:${USDC_BASE_CONTRACT}@${BASE_CHAIN_ID2}/transfer?address=${address}`,
    ethereum: `ethereum:${address}@${BASE_CHAIN_ID2}`,
    blockrun: `https://blockrun.ai/fund?address=${address}`
  };
}
function formatWalletCreatedMessage(address) {
  const links = getPaymentLinks(address);
  return `
I'm your BlockRun Agent! I can access GPT-4, Grok, image generation, and more.

Please send $1-5 USDC on Base to start:

${address}

What is Base? Base is Coinbase's blockchain network.
You can buy USDC on Coinbase and send it directly to me.

What $1 USDC gets you:
- ~1,000 GPT-4o calls
- ~100 image generations
- ~10,000 DeepSeek calls

Quick links:
- Check my balance: ${links.basescan}
- Get USDC: https://www.coinbase.com or https://bridge.base.org

Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues

Key stored securely in ~/.blockrun/
Your private key never leaves your machine - only signatures are sent.
`;
}
function formatNeedsFundingMessage(address) {
  const links = getPaymentLinks(address);
  return `
I've run out of funds! Please send more USDC on Base to continue helping you.

Send to my address:
${address}

Check my balance: ${links.basescan}

What $1 USDC gets you: ~1,000 GPT-4o calls or ~100 images.
Questions? care@blockrun.ai | Issues? github.com/BlockRunAI/blockrun-llm-ts/issues

Your private key never leaves your machine - only signatures are sent.
`;
}
function formatFundingMessageCompact(address) {
  const links = getPaymentLinks(address);
  return `I need a little top-up to keep helping you! Send USDC on Base to: ${address}
Check my balance: ${links.basescan}`;
}
var WALLET_FILE_PATH = walletFile();
var WALLET_DIR_PATH = walletDir();

// src/solana-wallet.ts
var fs5 = __toESM(require("fs"), 1);
var path4 = __toESM(require("path"), 1);
var os3 = __toESM(require("os"), 1);
var WALLET_DIR = path4.join(os3.homedir(), ".blockrun");
var SOLANA_WALLET_FILE = path4.join(WALLET_DIR, ".solana-session");
async function createSolanaWallet() {
  const { Keypair } = await loadSolanaWeb3("Solana wallet operations");
  const bs583 = await import("bs58");
  const keypair = Keypair.generate();
  return {
    address: keypair.publicKey.toBase58(),
    privateKey: (bs583.default ?? bs583).encode(keypair.secretKey)
  };
}
async function solanaKeyToBytes(privateKey) {
  const key = privateKey.trim();
  if (key.startsWith("[")) {
    let parsed;
    try {
      parsed = JSON.parse(key);
    } catch {
      throw new Error(
        "Invalid Solana private key: looks like a JSON byte array but is not valid JSON"
      );
    }
    if (!Array.isArray(parsed) || !parsed.every((n) => Number.isInteger(n) && n >= 0 && n <= 255)) {
      throw new Error(
        "Invalid Solana private key: JSON array must contain only byte values (0-255)"
      );
    }
    if (parsed.length !== 64) {
      throw new Error(
        `Invalid Solana key length: expected 64 bytes, got ${parsed.length}`
      );
    }
    return Uint8Array.from(parsed);
  }
  const hex = key.startsWith("0x") || key.startsWith("0X") ? key.slice(2) : key;
  if (/^[0-9a-fA-F]+$/.test(hex) && (hex.length === 64 || hex.length === 128)) {
    if (hex.length === 64) {
      throw new Error(
        "Invalid Solana private key: this is a 32-byte hex key \u2014 the EVM (Base) wallet format, not a Solana key. Solana keys are 64 bytes, usually base58-encoded (86-88 characters)."
      );
    }
    const bytes = new Uint8Array(64);
    for (let i = 0; i < 64; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
    return bytes;
  }
  try {
    const bs583 = await import("bs58");
    const bytes = (bs583.default ?? bs583).decode(key);
    if (bytes.length !== 64) {
      throw new Error(`Invalid Solana key length: expected 64 bytes, got ${bytes.length}`);
    }
    return bytes;
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(
      `Invalid Solana private key: ${msg}. Expected a base58-encoded 64-byte key (standard Solana format), a 64-byte hex string, or a Solana CLI JSON byte array.`,
      { cause: err }
    );
  }
}
async function solanaPublicKey(privateKey) {
  const { Keypair } = await loadSolanaWeb3("Solana wallet operations");
  const bytes = await solanaKeyToBytes(privateKey);
  return Keypair.fromSecretKey(bytes).publicKey.toBase58();
}
function saveSolanaWallet(privateKey) {
  if (!fs5.existsSync(WALLET_DIR)) fs5.mkdirSync(WALLET_DIR, { recursive: true });
  fs5.writeFileSync(SOLANA_WALLET_FILE, privateKey, { mode: 384 });
  return SOLANA_WALLET_FILE;
}
function scanSolanaWallets() {
  const home = os3.homedir();
  const results = [];
  try {
    const entries = fs5.readdirSync(home, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.name.startsWith(".") || !entry.isDirectory()) continue;
      const solanaWalletFile = path4.join(home, entry.name, "solana-wallet.json");
      if (fs5.existsSync(solanaWalletFile)) {
        try {
          const data = JSON.parse(fs5.readFileSync(solanaWalletFile, "utf-8"));
          const pk = data.privateKey || "";
          const addr = data.address || "";
          if (pk && addr) {
            const mtime = fs5.statSync(solanaWalletFile).mtimeMs;
            results.push({ mtime, secretKey: pk, publicKey: addr, source: solanaWalletFile });
          }
        } catch {
        }
      }
      if (entry.name === ".brcc") {
        const brccWalletFile = path4.join(home, entry.name, "wallet.json");
        if (fs5.existsSync(brccWalletFile)) {
          try {
            const data = JSON.parse(fs5.readFileSync(brccWalletFile, "utf-8"));
            const pk = data.privateKey || "";
            const addr = data.address || "";
            if (pk && addr) {
              const mtime = fs5.statSync(brccWalletFile).mtimeMs;
              results.push({ mtime, secretKey: pk, publicKey: addr, source: brccWalletFile });
            }
          } catch {
          }
        }
      }
    }
  } catch {
  }
  results.sort((a, b) => b.mtime - a.mtime);
  return results.map(({ secretKey, publicKey, source }) => ({ secretKey, publicKey, source }));
}
function loadSolanaWallet() {
  if (fs5.existsSync(SOLANA_WALLET_FILE)) {
    const key = fs5.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
    if (key) return key;
  }
  return null;
}
async function listDiscoveredSolanaWallets() {
  const listed = [];
  for (const entry of scanSolanaWallets()) {
    try {
      listed.push({ address: await solanaPublicKey(entry.secretKey), source: entry.source });
    } catch {
      continue;
    }
  }
  return listed;
}
async function importSolanaWallet(address) {
  const wanted = address.trim();
  for (const entry of scanSolanaWallets()) {
    let derived;
    try {
      derived = await solanaPublicKey(entry.secretKey);
    } catch {
      continue;
    }
    if (derived !== wanted) continue;
    if (fs5.existsSync(SOLANA_WALLET_FILE)) {
      const current = fs5.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
      if (current && current !== entry.secretKey) {
        const backup = path4.join(
          WALLET_DIR,
          `.solana-session.backup-${Math.floor(Date.now() / 1e3)}`
        );
        fs5.writeFileSync(backup, current, { mode: 384 });
      }
    }
    saveSolanaWallet(entry.secretKey);
    return derived;
  }
  const available = (await listDiscoveredSolanaWallets()).map((w) => w.address);
  throw new Error(
    `No discovered wallet controls ${address}. Available: ${available.length ? available.join(", ") : "none"}`
  );
}
async function formatSolanaWalletMigrationNotice(newAddress) {
  let discovered;
  try {
    discovered = scanSolanaWallets();
  } catch {
    return null;
  }
  const addresses = [];
  for (const entry of discovered) {
    try {
      addresses.push(await solanaPublicKey(entry.secretKey));
    } catch {
      continue;
    }
  }
  if (addresses.length === 0) return null;
  const found = addresses.map((addr) => `  ${addr}`).join("\n");
  return `
NOTICE: BlockRun created a new Solana wallet, but also found existing
wallet(s) belonging to other applications on this system:

${found}

BlockRun now uses only its own wallet:

  ${newAddress}

Discovered wallets are never adopted automatically \u2014 one may belong to a
different application, or have been planted to make you fund an address you
do not control.

If an address above is yours and holds your USDC, adopt it deliberately:

  import { importSolanaWallet } from '@blockrun/llm';
  await importSolanaWallet("<address-from-the-list-above>");

Your current wallet is backed up first. You can also set
SOLANA_WALLET_KEY=<private-key> for a single run without changing anything.
`;
}
async function solanaPublicKeyFrom(privateKey, source) {
  try {
    return await solanaPublicKey(privateKey);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(`${msg} (key loaded from ${source})`, { cause: err });
  }
}
async function getOrCreateSolanaWallet() {
  const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
  if (envKey) {
    const address2 = await solanaPublicKeyFrom(
      envKey,
      "the SOLANA_WALLET_KEY environment variable"
    );
    return { privateKey: envKey, address: address2, isNew: false };
  }
  if (fs5.existsSync(SOLANA_WALLET_FILE)) {
    const fileKey = fs5.readFileSync(SOLANA_WALLET_FILE, "utf-8").trim();
    if (fileKey) {
      const address2 = await solanaPublicKeyFrom(fileKey, SOLANA_WALLET_FILE);
      return { privateKey: fileKey, address: address2, isNew: false };
    }
  }
  const { address, privateKey } = await createSolanaWallet();
  saveSolanaWallet(privateKey);
  return { address, privateKey, isNew: true };
}

// src/poll-ownership.ts
var import_crypto = require("crypto");
var import_bs58 = __toESM(require("bs58"), 1);
var POLL_AUTH_HEADERS = {
  wallet: "x-poll-wallet",
  timestamp: "x-poll-timestamp",
  signature: "x-poll-signature"
};
function pollAuthMessage(kind, jobId, timestampS) {
  return `blockrun-poll:v1:${kind}:${jobId}:${timestampS}`;
}
function pollJobIdFromUrl(pollUrl) {
  const path7 = new URL(pollUrl, "https://placeholder.invalid").pathname.replace(/\/+$/, "");
  const segment = path7.slice(path7.lastIndexOf("/") + 1);
  if (!segment) throw new Error(`poll_url has no job id segment: ${pollUrl}`);
  return decodeURIComponent(segment);
}
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
function signPollOwnership(secretKey, kind, jobId, nowMs = Date.now()) {
  if (secretKey.length !== 64 && secretKey.length !== 32) {
    throw new Error(`Solana secret key must be 64 bytes (or a 32-byte seed), got ${secretKey.length}`);
  }
  const seed = Buffer.from(secretKey.subarray(0, 32));
  const privateKey = (0, import_crypto.createPrivateKey)({
    key: Buffer.concat([ED25519_PKCS8_PREFIX, seed]),
    format: "der",
    type: "pkcs8"
  });
  const jwk = (0, import_crypto.createPublicKey)(privateKey).export({ format: "jwk" });
  if (!jwk.x) throw new Error("Could not derive the Ed25519 public key");
  const publicKey = Buffer.from(jwk.x, "base64url");
  const timestampS = Math.floor(nowMs / 1e3);
  const signature = (0, import_crypto.sign)(null, Buffer.from(pollAuthMessage(kind, jobId, timestampS), "utf8"), privateKey);
  return {
    [POLL_AUTH_HEADERS.wallet]: import_bs58.default.encode(publicKey),
    [POLL_AUTH_HEADERS.timestamp]: String(timestampS),
    [POLL_AUTH_HEADERS.signature]: import_bs58.default.encode(signature)
  };
}

// src/solana-batch.ts
var import_async_hooks = require("async_hooks");
var import_crypto2 = require("crypto");
var fs6 = __toESM(require("fs"), 1);
var path5 = __toESM(require("path"), 1);
var import_bs582 = __toESM(require("bs58"), 1);
var BATCH_SCHEME = "batch-settlement";
var BLOCKRUN_SOL_OPERATOR = "5YKPQUFjw5WQqhSUkEGKNNfYYVqnRRNbpYyL71qQ1vm3";
var DEFAULT_BATCH_RATE_LIMIT = { maxAttempts: 3, maxWaitMs: 6e4 };
var DEFAULT_BATCH_IN_DOUBT = { maxWaitMs: 3e5 };
var DEFAULT_BATCH_LOCK_WAIT_MS = 6e4;
var LOCK_POLL_MS = 500;
var BatchPaymentUnresolvedError = class extends PaymentError {
  /** The {@link SolanaBatchEvent.reason} of the `unresolved` event reported with it. */
  reason;
  /** The paying wallet's address. */
  wallet;
  /** The request id of the payment that may have been charged. */
  requestId;
  /** The channel it paid into. */
  channelId;
  /** What it carried. */
  payloadKind;
  /** HTTP status of the gateway's last answer to it, when there was one. */
  status;
  /** Whether it carried a deposit, which is now in doubt as well. */
  depositInDoubt;
  constructor(init) {
    super(
      `batch-settlement payment ${init.requestId ?? "<unknown>"} from wallet ${init.wallet} may have been charged and is not paid again${init.detail ? `: ${init.detail}` : ""}`
    );
    this.name = "BatchPaymentUnresolvedError";
    withDisposition(this, "paid-or-in-doubt");
    this.reason = init.reason;
    this.wallet = init.wallet;
    this.requestId = init.requestId;
    this.channelId = init.channelId;
    this.payloadKind = init.payloadKind;
    this.status = init.status;
    this.depositInDoubt = init.payloadKind === "open" || init.payloadKind === "top-up";
    if (init.cause !== void 0) this.cause = init.cause;
  }
};
var BatchCloseDeferredError = class extends BlockrunError {
  reason;
  /** The paying wallet's address. */
  wallet;
  /** The channels whose deposit is in doubt (`deposit_in_doubt`). */
  channelIds;
  constructor(init) {
    super(`batch-settlement channel close deferred for wallet ${init.wallet}: ${init.detail}`);
    this.name = "BatchCloseDeferredError";
    this.reason = init.reason;
    this.wallet = init.wallet;
    this.channelIds = init.channelIds ?? [];
  }
};
var BatchChannelLockedError = class extends BlockrunError {
  /** The paying wallet's address. */
  wallet;
  /** The live owner's pid, when the SDK knows it. */
  ownerPid;
  /** How long this call waited for the lock, in ms (0 unless `onLocked` is `"wait"`). */
  waitedMs;
  constructor(init) {
    super(`batch-settlement unavailable for wallet ${init.wallet}: ${init.detail}`);
    this.name = "BatchChannelLockedError";
    this.wallet = init.wallet;
    if (init.ownerPid !== void 0) this.ownerPid = init.ownerPid;
    this.waitedMs = init.waitedMs;
    withDisposition(this, "unpaid");
  }
};
var INSTALL_HINT2 = "npm install @x402/core@~2.28.0 @x402/svm@~2.28.0 @solana/kit";
async function load(pkg, importer) {
  try {
    return await importer();
  } catch (err) {
    throw new Error(
      `@blockrun/llm: Solana batch-settlement requires the optional peer dependency "${pkg}", which is not installed.

  ${INSTALL_HINT2}

Original error: ${err instanceof Error ? err.message : String(err)}`,
      { cause: err }
    );
  }
}
var FileChannelStorage = class {
  constructor(file) {
    this.file = file;
  }
  file;
  read() {
    try {
      const parsed = JSON.parse(fs6.readFileSync(this.file, "utf8"));
      return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
    } catch (err) {
      if (err.code === "ENOENT") return {};
      throw err;
    }
  }
  write(all) {
    fs6.mkdirSync(path5.dirname(this.file), { recursive: true, mode: 448 });
    const tmp = `${this.file}.${process.pid}.tmp`;
    fs6.writeFileSync(tmp, JSON.stringify(all, null, 2), { mode: 384 });
    fs6.renameSync(tmp, this.file);
  }
  async get(key) {
    return this.read()[key];
  }
  async set(key, record) {
    const all = this.read();
    all[key] = record;
    this.write(all);
  }
  async delete(key) {
    const all = this.read();
    if (!(key in all)) return;
    delete all[key];
    this.write(all);
  }
  /** The keys of every stored channel. */
  async keys() {
    return Object.keys(this.read());
  }
  /** Forget every channel in this file. */
  clear() {
    try {
      fs6.unlinkSync(this.file);
    } catch (err) {
      if (err.code !== "ENOENT") throw err;
    }
  }
};
var MemoryChannelStorage = class {
  records = /* @__PURE__ */ new Map();
  async get(key) {
    const raw = this.records.get(key);
    return raw === void 0 ? void 0 : JSON.parse(raw);
  }
  async set(key, record) {
    this.records.set(key, JSON.stringify(record));
  }
  async delete(key) {
    this.records.delete(key);
  }
  async keys() {
    return [...this.records.keys()];
  }
  clear() {
    this.records.clear();
  }
};
function isPlainObject(value) {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}
function intentProblem(key, intent) {
  const atomic = (value) => typeof value === "string" && /^\d+$/.test(value);
  if (!isPlainObject(intent)) return "not an object";
  if (intent.key !== key || !key) return "key does not match its entry";
  if (typeof intent.channelId !== "string" || !intent.channelId) return "channelId missing";
  if (!isPlainObject(intent.channelConfig)) return "channelConfig missing";
  if (intent.kind !== "open" && intent.kind !== "top-up") return "kind is neither open nor top-up";
  if (!atomic(intent.cumulative)) return "cumulative is not an atomic amount";
  if (!atomic(intent.expectDeposit)) return "expectDeposit is not an atomic amount";
  if (intent.knownDeposit !== void 0 && !atomic(intent.knownDeposit)) return "knownDeposit is not an atomic amount";
  if (intent.requestId !== void 0 && typeof intent.requestId !== "string") return "requestId is not a string";
  if (intent.anchorHeight !== void 0 && (!Number.isSafeInteger(intent.anchorHeight) || intent.anchorHeight < 0)) {
    return "anchorHeight is not a block height";
  }
  if (typeof intent.at !== "number" || !Number.isFinite(intent.at)) return "at is not a time";
  return void 0;
}
function fsyncDirectory(dir) {
  let fd;
  try {
    fd = fs6.openSync(dir, "r");
    fs6.fsyncSync(fd);
  } catch (err) {
    const code = err.code;
    if (!["EISDIR", "EPERM", "EACCES", "EINVAL", "ENOTSUP", "EBADF"].includes(code ?? "")) throw err;
  } finally {
    if (fd !== void 0) fs6.closeSync(fd);
  }
}
var FileIntentJournal = class _FileIntentJournal {
  constructor(file) {
    this.file = file;
  }
  file;
  durable = true;
  /** The journal file for a channel store file: its full path plus `.deposit-intents`, one per store. */
  static beside(channelFile) {
    return new _FileIntentJournal(`${channelFile}.deposit-intents`);
  }
  /**
   * Where earlier builds kept a store's journal: the store's name without
   * `.json`, plus `.deposit-intents.json`. Two stores could share it
   * (`/x/channels` and `/x/channels.json`), so its intents are moved by payer
   * ({@link adoptLegacy}). No current journal path ever ends in `.json`, so
   * this never names one.
   */
  static legacyBeside(channelFile) {
    const base = path5.basename(channelFile).replace(/\.json$/i, "");
    return path5.join(path5.dirname(channelFile), `${base}.deposit-intents.json`);
  }
  /**
   * Move `payer`'s intents from a journal an earlier build wrote at
   * `legacyFile` into this one, before this one is used: written here first
   * (durably), then removed there (the file goes once empty). Intents of other
   * payers are left where they are, for the store they belong to. An intent
   * already here is kept (a migration interrupted between the two writes).
   *
   * @throws when the old journal cannot be read or is not valid, or holds an
   *   intent naming no payer: the wallet then keeps batch off, as for an
   *   unreadable journal, and nothing is dropped.
   */
  adoptLegacy(legacyFile, payer) {
    if (legacyFile === this.file) return;
    const legacy = new _FileIntentJournal(legacyFile);
    const found = legacy.read();
    const mine = [];
    const others = {};
    for (const [key, intent] of Object.entries(found)) {
      const owner = intent.channelConfig.payer;
      if (typeof owner !== "string") {
        throw new Error(`deposit-intent journal ${legacyFile} holds an intent naming no payer (${JSON.stringify(key)}); move it by hand`);
      }
      if (owner === payer) mine.push(intent);
      else others[key] = intent;
    }
    if (mine.length === 0) return;
    const intents = this.read();
    for (const intent of mine) intents[intent.key] ??= intent;
    this.write(intents);
    legacy.write(others);
  }
  /**
   * The journaled intents. Only a missing file is an empty journal: anything
   * else that is not a version-1 journal of well-formed intents throws, so a
   * deposit in doubt is never lost to a damaged or unknown file (the wallet
   * then keeps batch off, `deposit_journal_unreadable`).
   */
  read() {
    let text;
    try {
      text = fs6.readFileSync(this.file, "utf8");
    } catch (err) {
      if (err.code === "ENOENT") return {};
      throw err;
    }
    const invalid = (why) => new Error(`deposit-intent journal ${this.file} is not valid (${why})`);
    let parsed;
    try {
      parsed = JSON.parse(text);
    } catch (err) {
      throw invalid(err instanceof Error ? err.message : String(err));
    }
    if (!isPlainObject(parsed)) throw invalid("not a JSON object");
    if (parsed.version !== 1) throw invalid(`unsupported version ${JSON.stringify(parsed.version)}`);
    const { intents } = parsed;
    if (!isPlainObject(intents)) throw invalid("intents is not an object");
    for (const [key, intent] of Object.entries(intents)) {
      const why = intentProblem(key, intent);
      if (why) throw invalid(`intent ${JSON.stringify(key)}: ${why}`);
    }
    return intents;
  }
  write(intents) {
    const dir = path5.dirname(this.file);
    fs6.mkdirSync(dir, { recursive: true, mode: 448 });
    if (Object.keys(intents).length === 0) {
      try {
        fs6.unlinkSync(this.file);
      } catch (err) {
        if (err.code !== "ENOENT") throw err;
        return;
      }
      fsyncDirectory(dir);
      return;
    }
    const tmp = `${this.file}.${process.pid}.${(0, import_crypto2.randomUUID)()}.tmp`;
    const fd = fs6.openSync(tmp, "w", 384);
    try {
      fs6.writeSync(fd, JSON.stringify({ version: 1, intents }, null, 2));
      fs6.fsyncSync(fd);
    } finally {
      fs6.closeSync(fd);
    }
    fs6.renameSync(tmp, this.file);
    fsyncDirectory(dir);
  }
  list() {
    return Object.values(this.read());
  }
  put(intent) {
    const intents = this.read();
    intents[intent.key] = intent;
    this.write(intents);
  }
  remove(key) {
    const intents = this.read();
    if (!(key in intents)) return;
    delete intents[key];
    this.write(intents);
  }
};
var MemoryIntentJournal = class {
  durable = false;
  intents = /* @__PURE__ */ new Map();
  list() {
    return [...this.intents.values()];
  }
  put(intent) {
    this.intents.set(intent.key, { ...intent });
  }
  remove(key) {
    this.intents.delete(key);
  }
};
function processAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch (err) {
    return err.code === "EPERM";
  }
}
var BATCH_REGISTRY = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-registry/v1");
var registryHost = globalThis;
var registry2 = registryHost[BATCH_REGISTRY] ??= {
  wallets: /* @__PURE__ */ new Map(),
  locks: /* @__PURE__ */ new Map(),
  exitHook: false
};
var lockIds = registry2.lockIds ??= /* @__PURE__ */ new Map();
function fileIdentity(file) {
  try {
    const stat = fs6.statSync(file);
    return `${stat.dev}:${stat.ino}:${stat.mtimeMs}`;
  } catch {
    return void 0;
  }
}
function lockPredatesProcess(lock) {
  try {
    return fs6.statSync(lock).mtimeMs < performance.timeOrigin;
  } catch {
    return false;
  }
}
function holdsChannelFile(file) {
  const token = registry2.locks.get(file);
  if (!token) return false;
  const lock = `${file}.lock`;
  if (!ownsLock(lock, token)) return false;
  const id = lockIds.get(file);
  return id === void 0 || fileIdentity(lock) === id;
}
function lockTokenFile(lock) {
  return `${lock}.owner`;
}
function readLockOwner(lock) {
  let raw;
  try {
    raw = fs6.readFileSync(lock, "utf8");
  } catch (err) {
    if (err.code === "ENOENT") return void 0;
    throw err;
  }
  let pid = Number(raw.trim());
  if (!Number.isInteger(pid)) {
    try {
      const parsed = JSON.parse(raw);
      pid = parsed && typeof parsed === "object" && Number.isInteger(parsed.pid) ? parsed.pid : 0;
    } catch {
      pid = 0;
    }
  }
  return { pid, raw };
}
function readLockToken(lock) {
  try {
    const parsed = JSON.parse(fs6.readFileSync(lockTokenFile(lock), "utf8"));
    return parsed && typeof parsed === "object" && typeof parsed.token === "string" ? parsed.token : void 0;
  } catch {
    return void 0;
  }
}
function ownsLock(lock, token) {
  return readLockOwner(lock)?.pid === process.pid && readLockToken(lock) === token;
}
function releaseHeldLocks() {
  for (const [file, token] of registry2.locks) {
    const lock = `${file}.lock`;
    try {
      if (!ownsLock(lock, token)) continue;
      fs6.unlinkSync(lockTokenFile(lock));
      fs6.unlinkSync(lock);
    } catch {
    }
  }
}
function lockChannelFile(file) {
  if (registry2.locks.has(file)) return void 0;
  const lock = `${file}.lock`;
  fs6.mkdirSync(path5.dirname(lock), { recursive: true, mode: 448 });
  const token = (0, import_crypto2.randomUUID)();
  for (let attempt = 0; attempt < 2; attempt += 1) {
    const tmp = `${lock}.${process.pid}.${token}.tmp`;
    try {
      fs6.writeFileSync(tmp, String(process.pid), { mode: 384 });
      try {
        fs6.linkSync(tmp, lock);
      } catch (err) {
        const code = err.code;
        if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "EOPNOTSUPP") throw err;
        fs6.writeFileSync(lock, String(process.pid), { flag: "wx", mode: 384 });
      }
      try {
        writeLockToken(lock, token);
      } catch (err) {
        try {
          fs6.unlinkSync(lock);
        } catch {
        }
        throw err;
      }
      registry2.locks.set(file, token);
      const id = fileIdentity(lock);
      if (id) lockIds.set(file, id);
      else lockIds.delete(file);
      if (!registry2.exitHook) {
        registry2.exitHook = true;
        process.once("exit", releaseHeldLocks);
      }
      return void 0;
    } catch (err) {
      if (err.code !== "EEXIST") throw err;
    } finally {
      try {
        fs6.unlinkSync(tmp);
      } catch {
      }
    }
    const owner = readLockOwner(lock);
    if (!owner) continue;
    if (owner.pid === process.pid) {
      if (!lockPredatesProcess(lock)) return process.pid;
    } else if (owner.pid > 0 && processAlive(owner.pid)) {
      return owner.pid;
    }
    try {
      if (readLockOwner(lock)?.raw === owner.raw) fs6.unlinkSync(lock);
    } catch {
    }
  }
  return -1;
}
function writeLockToken(lock, token) {
  const sidecar = lockTokenFile(lock);
  const tmp = `${sidecar}.${process.pid}.${token}.tmp`;
  try {
    fs6.writeFileSync(tmp, JSON.stringify({ pid: process.pid, token }), { mode: 384 });
    fs6.renameSync(tmp, sidecar);
  } finally {
    try {
      fs6.unlinkSync(tmp);
    } catch {
    }
  }
}
function pendingDeposit(pending) {
  const entries = Array.isArray(pending) ? pending : [pending];
  return entries.some(
    (entry) => entry?.payment?.payload?.type === "deposit"
  );
}
var ChannelResyncRequiredError = class extends Error {
  constructor(channelId) {
    super(`batch-settlement channel ${channelId ?? "<unknown>"} needs a chain re-read before it is used`);
    this.channelId = channelId;
    this.name = "ChannelResyncRequiredError";
  }
  channelId;
};
function isResyncRequired(err) {
  return err instanceof Error && err.name === "ChannelResyncRequiredError";
}
function parseAtomic(value) {
  return typeof value === "string" && /^\d+$/.test(value) ? BigInt(value) : 0n;
}
function confirmedCumulative(record, channelId) {
  if (!record || record.channelId !== channelId) return 0n;
  return record.pending === void 0 || record.hasConfirmedState ? parseAtomic(record.chargedCumulativeAmount) : 0n;
}
var ChannelBook = class {
  constructor(base, onUnverified = () => {
  }) {
    this.base = base;
    this.onUnverified = onUnverified;
  }
  base;
  onUnverified;
  seen = /* @__PURE__ */ new Map();
  async get(key) {
    const record = await this.base.get(key);
    if (record) this.seen.set(key, record);
    if (!record?.pending || record.channelConfig?.voucherSigner !== "server") return record;
    if (record.hasConfirmedState && !pendingDeposit(record.pending)) {
      const { pending: _pending, hasConfirmedState: _confirmed, ...confirmed } = record;
      await this.set(key, confirmed);
      return confirmed;
    }
    if (typeof record.channelId === "string") {
      const pendingEntries = Array.isArray(record.pending) ? record.pending : [record.pending];
      const inDoubt = pendingEntries.find((entry) => entry?.payment?.payload?.type === "deposit");
      this.onUnverified({
        key,
        channelId: record.channelId,
        channelConfig: record.channelConfig,
        cumulative: record.hasConfirmedState ? parseAtomic(record.chargedCumulativeAmount) : 0n,
        ...record.hasConfirmedState ? { knownDeposit: parseAtomic(record.deposit) } : {},
        ...inDoubt ? { expectDeposit: parseAtomic(inDoubt.deposit) } : {},
        reason: "orphaned_deposit"
      });
    }
    throw new ChannelResyncRequiredError(record.channelId);
  }
  /**
   * Store a record, and only once it is stored remember it as the last one
   * seen. If storing fails (say, while the scheme saves a reconciled
   * receipt), the scheme reports the receipt unreconciled, and the SDK must
   * still see the state from before it: `distrust()` adds that call's charge
   * to it, and a remembered post-receipt cumulative would count it twice.
   */
  async set(key, record) {
    await this.base.set(key, record);
    this.seen.set(key, record);
  }
  async delete(key) {
    await this.base.delete(key);
  }
  /** The keys of every stored channel. */
  async keys() {
    return await this.base.keys?.() ?? [];
  }
  /** The last record seen for a channel, even one the scheme has deleted since. */
  find(channelId) {
    for (const [key, record] of this.seen) {
      if (record.channelId === channelId) return { key, record };
    }
    return void 0;
  }
  clear() {
    this.seen.clear();
    this.base.clear?.();
  }
  /** Forget one channel: its stored record and the last record seen for it. An emptied store is removed. */
  async forget(key) {
    this.seen.delete(key);
    await this.base.delete(key);
    if ((await this.keys()).length === 0) this.clear();
  }
};
var PAYMENT_CHANNELS_PROGRAM_ID = "CHNLxYvVA28MJP9PrFuDXccuoGXAx7jBacfLEkahyGsX";
var CHANNEL = {
  size: 256,
  rentPayer: 216,
  discriminator: 1,
  status: 3,
  deposit: 12,
  settled: 20,
  closureStartedAt: 36,
  payer: 88,
  authorizedSigner: 152,
  mint: 184
};
function decodeChannelAccount(owner, data) {
  if (owner !== PAYMENT_CHANNELS_PROGRAM_ID || data.length < CHANNEL.size || data[0] !== CHANNEL.discriminator) {
    return void 0;
  }
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  const key = (offset) => import_bs582.default.encode(data.subarray(offset, offset + 32));
  return {
    deposit: view.getBigUint64(CHANNEL.deposit, true),
    settled: view.getBigUint64(CHANNEL.settled, true),
    open: data[CHANNEL.status] === 0 && view.getBigInt64(CHANNEL.closureStartedAt, true) === 0n,
    payer: key(CHANNEL.payer),
    authorizedSigner: key(CHANNEL.authorizedSigner),
    mint: key(CHANNEL.mint)
  };
}
var ChannelUnreadableError = class extends Error {
  constructor(channelId, why) {
    super(`account ${channelId} is on chain but is not a readable payment channel (${why})`);
    this.name = "ChannelUnreadableError";
  }
};
var ChannelLayoutError = class extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = "ChannelLayoutError";
  }
};
async function readChannelAccount(rpcUrl, channelId, rpcHeaders, options = {}) {
  await checkChannelLayout();
  const config = { encoding: "base64", commitment: options.commitment ?? "finalized" };
  if (options.minContextSlot !== void 0) config.minContextSlot = options.minContextSlot;
  const response = await fetch(rpcUrl, {
    method: "POST",
    headers: { ...rpcHeaders, "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getAccountInfo", params: [channelId, config] }),
    signal: AbortSignal.timeout(15e3),
    // rpcHeaders may carry an API key: never let a redirect take it elsewhere.
    redirect: "error"
  });
  if (!response.ok) throw new Error(`getAccountInfo answered HTTP ${response.status}`);
  const body = await response.json();
  if (body.error) throw new Error(`getAccountInfo failed: ${body.error.message ?? JSON.stringify(body.error)}`);
  if (!body.result || typeof body.result !== "object" || !("value" in body.result)) {
    throw new Error("getAccountInfo returned no result");
  }
  const value = body.result.value;
  if (value === null) return void 0;
  if (!value || typeof value !== "object") throw new ChannelUnreadableError(channelId, "malformed account value");
  if (!Array.isArray(value.data) || typeof value.data[0] !== "string" || value.data[1] !== "base64") {
    throw new ChannelUnreadableError(channelId, "unsupported data encoding");
  }
  const channel = decodeChannelAccount(value.owner, Buffer.from(value.data[0], "base64"));
  if (!channel) throw new ChannelUnreadableError(channelId, "wrong owner or layout");
  return channel;
}
async function rpcRequest(rpcUrl, method, params, rpcHeaders) {
  const response = await fetch(rpcUrl, {
    method: "POST",
    headers: { ...rpcHeaders, "Content-Type": "application/json" },
    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
    signal: AbortSignal.timeout(15e3),
    // rpcHeaders may carry an API key: never let a redirect take it elsewhere.
    redirect: "error"
  });
  if (!response.ok) throw new Error(`${method} answered HTTP ${response.status}`);
  const body = await response.json();
  if (body.error) throw new Error(`${method} failed: ${body.error.message ?? JSON.stringify(body.error)}`);
  if (body.result === void 0 || body.result === null) throw new Error(`${method} returned no result`);
  return body.result;
}
function rpcInteger(value, what) {
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${what} is not a block height or slot`);
  return value;
}
var layoutChecked;
function checkChannelLayout() {
  layoutChecked ??= (async () => {
    const svm = await load("@x402/svm", () => import("@x402/svm")).catch((err) => {
      throw new ChannelLayoutError(
        `cannot check the payment-channel account layout: ${err instanceof Error ? err.message : String(err)}`,
        { cause: err }
      );
    });
    if (svm.CHANNEL_ACCOUNT_SIZE !== BigInt(CHANNEL.size) || svm.CHANNEL_RENT_PAYER_OFFSET !== BigInt(CHANNEL.rentPayer)) {
      throw new ChannelLayoutError("@x402/svm changed the payment-channel account layout; this SDK cannot read channels from it");
    }
  })();
  layoutChecked.catch(() => {
    layoutChecked = void 0;
  });
  return layoutChecked;
}
var LANDING_MARGIN_BLOCKS = 300;
function savedReclaimAnchor(record) {
  const saved = record.reclaimAnchorHeight;
  return typeof saved === "number" && Number.isSafeInteger(saved) && saved > 0 ? saved : void 0;
}
function maxOf(a, b) {
  if (a === void 0) return b;
  if (b === void 0) return a;
  return a > b ? a : b;
}
var ResyncError = class extends Error {
  constructor(reason, message) {
    super(message);
    this.reason = reason;
    this.name = "ResyncError";
  }
  reason;
};
function payloadChannelId(payload) {
  return payload.payload?.authorization?.channelId ?? payload.payload?.voucher?.channelId;
}
function clientSignedBatch(accept) {
  return accept.scheme === BATCH_SCHEME && accept.extra?.voucherSigner !== "server";
}
function batchUnavailable(paymentRequired) {
  const batch = (paymentRequired.accepts ?? []).filter((accept) => accept.scheme === BATCH_SCHEME);
  if (batch.length === 0) return { reason: "not_offered" };
  if (!batch.every(clientSignedBatch)) return void 0;
  return {
    reason: "client_signed_not_supported",
    detail: "the 402 offers only client-signed batch-settlement accepts; this SDK pays batch only into server-signed (operator) channels"
  };
}
function withoutClientSignedBatch(paymentRequired) {
  const accepts = paymentRequired.accepts ?? [];
  return accepts.some(clientSignedBatch) ? { ...paymentRequired, accepts: accepts.filter((accept) => !clientSignedBatch(accept)) } : paymentRequired;
}
var DEPOSIT_BLOCKHASH_TTL_MS = 1e4;
var MAX_DEPOSIT_BLOCKHASH_ENDPOINTS = 8;
var depositBlockhashes = /* @__PURE__ */ new Map();
async function finalizedBlockhash(rpcUrl, rpcHeaders) {
  const cached = depositBlockhashes.get(rpcUrl);
  if (cached && Date.now() - cached.fetchedAt < DEPOSIT_BLOCKHASH_TTL_MS) return cached;
  const result = await rpcRequest(rpcUrl, "getLatestBlockhash", [{ commitment: "finalized" }], rpcHeaders);
  const blockhash = result.value?.blockhash;
  if (typeof blockhash !== "string" || !blockhash) throw new Error("getLatestBlockhash returned no blockhash");
  const entry = {
    blockhash,
    lastValidBlockHeight: rpcInteger(result.value?.lastValidBlockHeight, "getLatestBlockhash lastValidBlockHeight"),
    fetchedAt: Date.now()
  };
  depositBlockhashes.delete(rpcUrl);
  depositBlockhashes.set(rpcUrl, entry);
  while (depositBlockhashes.size > MAX_DEPOSIT_BLOCKHASH_ENDPOINTS) {
    depositBlockhashes.delete(depositBlockhashes.keys().next().value);
  }
  return entry;
}
function withDepositBlockhash(paymentRequired, blockhash) {
  const accepts = paymentRequired.accepts ?? [];
  const lacking = (accept) => accept.scheme === BATCH_SCHEME && !accept.extra?.recentBlockhash;
  if (!accepts.some(lacking)) return paymentRequired;
  return {
    ...paymentRequired,
    accepts: accepts.map(
      (accept) => lacking(accept) ? { ...accept, extra: { ...accept.extra, recentBlockhash: blockhash.blockhash, lastValidBlockHeight: blockhash.lastValidBlockHeight } } : accept
    )
  };
}
var PAYMENT_IDENTIFIER = "payment-identifier";
function withPaymentIdentifier(paymentRequired) {
  const declared = paymentRequired.extensions?.[PAYMENT_IDENTIFIER];
  if (typeof declared?.info?.required !== "boolean") return paymentRequired;
  const id = `pay_${(0, import_crypto2.randomUUID)().replace(/-/g, "")}`;
  return {
    ...paymentRequired,
    extensions: { ...paymentRequired.extensions, [PAYMENT_IDENTIFIER]: { ...declared, info: { ...declared.info, id } } }
  };
}
function peekReceipt(http, getHeader) {
  try {
    return http.getPaymentSettleResponse(getHeader);
  } catch {
    return void 0;
  }
}
function receiptSucceeded(http, answer) {
  return peekReceipt(http, (name) => answer.headers.get(name))?.success === true;
}
function hasPaymentIdentifier(payload) {
  const info = payload.extensions?.[PAYMENT_IDENTIFIER]?.info;
  return typeof info?.id === "string";
}
var IN_DOUBT_MIN_WAIT_MS = 1e3;
var IN_DOUBT_MAX_WAIT_MS = 3e4;
function replayMayResolve(doubt) {
  if (doubt.unanswered) return true;
  const answer = doubt.held;
  if (!answer) return false;
  return answer.status >= 500 || answer.status === 429 || answer.status === 409 && Boolean(answer.headers.get("retry-after")?.trim());
}
function inDoubtWait(doubt, replay) {
  const asked = doubt.held ? parseRetryAfter(doubt.held.headers.get("retry-after")) : void 0;
  return Math.min(IN_DOUBT_MAX_WAIT_MS, Math.max(IN_DOUBT_MIN_WAIT_MS, asked ?? backoffDelay(replay)));
}
function replayDoubt(outcome) {
  if (outcome.kind === "charged" || outcome.kind === "in_doubt") return outcome;
  return { kind: "in_doubt", reason: "replay_unresolved", replayable: false, settled: true, detail: "the replay was refused" };
}
function trustedBatchAccepts(paymentRequired, operators) {
  return (paymentRequired.accepts ?? []).filter((candidate) => {
    const extra = candidate.extra;
    return candidate.scheme === BATCH_SCHEME && extra?.voucherSigner === "server" && operators.includes(extra.operator);
  });
}
function trustedBatchAccept(paymentRequired, operators) {
  return trustedBatchAccepts(paymentRequired, operators)[0];
}
function chargedUsd(getReceipt) {
  let receipt;
  try {
    receipt = getReceipt();
  } catch {
    return 0;
  }
  return receiptChargedUsd(receipt) ?? 0;
}
function rebuiltCharge(settled, payload) {
  if (settled?.success !== true) return 0n;
  const charged = parseAtomic(typeof settled.extra?.chargedAmount === "string" ? settled.extra.chargedAmount : settled.amount);
  const ceiling = parseAtomic(payload.accepted?.amount);
  return charged > ceiling ? ceiling : charged;
}
function confirmedState(record) {
  if (!record || !(record.pending === void 0 || record.hasConfirmedState)) return void 0;
  return { cumulative: parseAtomic(record.chargedCumulativeAmount), deposit: parseAtomic(record.deposit) };
}
function recordAdvanced(before, after, payload, receipt) {
  const deposited = payload.payload?.type === "deposit" ? parseAtomic(payload.payload.deposit?.amount) : 0n;
  const charged = rebuiltCharge(receipt, payload);
  if (deposited === 0n && charged === 0n) return true;
  if (!after) return false;
  const prior = before ?? { cumulative: 0n, deposit: 0n };
  return after.deposit >= prior.deposit + deposited && after.cumulative >= prior.cumulative + charged;
}
var BATCH_REFUSALS = /* @__PURE__ */ new Set([
  "batch_payer_not_allowed",
  "batch_payer_not_admitted",
  "batch_admission_paused",
  "batch_server_signed_only",
  "batch_unavailable",
  "batch_channel_limit",
  "PAYMENT_VERIFICATION_UNAVAILABLE"
]);
var PRE_REQUEST_REFUSALS = /* @__PURE__ */ new Set([
  "batch_payer_not_allowed",
  "batch_payer_not_admitted",
  "batch_admission_paused",
  "batch_server_signed_only",
  "batch_unavailable",
  "batch_channel_limit"
]);
async function batchRefusal(response) {
  if (![400, 403, 409, 503].includes(response.status)) return void 0;
  let body;
  try {
    body = await response.clone().json();
  } catch {
    return void 0;
  }
  const { error, code } = body ?? {};
  for (const value of [code, error]) {
    if (typeof value === "string" && BATCH_REFUSALS.has(value)) return value;
  }
  return void 0;
}
async function duplicateAnswer(response) {
  const codes = [];
  try {
    const body = await response.clone().json();
    if (body && typeof body === "object") codes.push(body.error, body.code, body.reason, body.invalidReason);
  } catch {
  }
  const header = response.headers.get("PAYMENT-REQUIRED");
  if (header) {
    try {
      const challenge = parsePaymentRequired(header);
      codes.push(challenge.error, challenge.invalidReason);
    } catch {
    }
  }
  for (const code of codes) {
    if (typeof code === "string" && /duplicate/i.test(code)) return code;
  }
  return void 0;
}
function cleanRefusalReceipt(receipt) {
  if (receipt === void 0) return true;
  return receipt.success === false && !receipt.transaction && receipt.errorReason !== "settlement_pending";
}
async function afterPaymentError(response) {
  let body;
  try {
    body = await response.clone().json();
  } catch {
    body = { error: "Request failed" };
  }
  return withDisposition(
    new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(body)),
    "paid-or-in-doubt"
  );
}
function cancelledReceipt(receipt) {
  return receipt?.success === false && receipt.errorReason === "batch_cancelled" && !receipt.transaction;
}
async function cancelledError(response) {
  let body;
  try {
    body = await response.clone().json();
  } catch {
    body = { error: "Request failed" };
  }
  return withDisposition(new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(body)), "unpaid");
}
function describeReceipt(receipt) {
  if (!receipt) return "no receipt";
  const parts = [`success=${String(receipt.success)}`];
  if (receipt.errorReason) parts.push(`errorReason=${receipt.errorReason}`);
  if (receipt.transaction) parts.push(`transaction=${receipt.transaction}`);
  return `receipt ${parts.join(" ")}`;
}
var MAX_RETRY_AFTER_MS = 24 * 60 * 60 * 1e3;
function parseRetryAfter(value, now = Date.now()) {
  const text = value?.trim();
  if (!text) return void 0;
  let ms;
  if (/^\d+(\.\d+)?$/.test(text)) {
    ms = Math.round(Number(text) * 1e3);
  } else {
    const at = Date.parse(text);
    if (Number.isNaN(at)) return void 0;
    ms = Math.max(0, at - now);
  }
  return Number.isFinite(ms) && ms <= MAX_RETRY_AFTER_MS ? ms : void 0;
}
var NOT_BROADCAST_REFUSALS = /* @__PURE__ */ new Set([
  // 429
  "batch_account_channel_capacity_exhausted",
  "batch_channel_capacity_exhausted",
  "batch_deposit_rate_limited",
  "batch_deposit_ip_rate_limited",
  // 503
  "batch_signer_funding_headroom_exhausted",
  "batch_new_channels_disabled",
  "batch_public_new_channels_disabled",
  // 403
  "batch_account_admission_paused",
  "batch_channel_owner_mismatch",
  "batch_fee_payer_unavailable",
  "batch_fee_payer_not_dedicated",
  "batch_fee_payer_lane_not_authorized",
  "batch_transaction_fee_payer_mismatch",
  // 400
  "batch_initial_deposit_out_of_range",
  "batch_deposit_asset_not_supported",
  "batch_withdraw_delay_out_of_range"
]);
function provesNothingBroadcast(receipt) {
  return receipt?.success === false && !receipt.transaction && typeof receipt.errorReason === "string" && NOT_BROADCAST_REFUSALS.has(receipt.errorReason);
}
var CONFIG_REFUSALS = /* @__PURE__ */ new Set([
  "batch_initial_deposit_out_of_range",
  "batch_deposit_asset_not_supported",
  "batch_withdraw_delay_out_of_range"
]);
var REFUSAL_COOLDOWN_MS = 6e4;
var CONFIG_REFUSAL_COOLDOWN_MS = 10 * 6e4;
var REFUSAL_COOLDOWN_MAX_MS = 15 * 6e4;
function refusalCooldownMs(errorReason, retryAfterMs2) {
  if (retryAfterMs2 !== void 0) return Math.min(retryAfterMs2, REFUSAL_COOLDOWN_MAX_MS);
  return errorReason !== void 0 && CONFIG_REFUSALS.has(errorReason) ? CONFIG_REFUSAL_COOLDOWN_MS : REFUSAL_COOLDOWN_MS;
}
function backoffDelay(attempt) {
  const base = 1e3 * 2 ** Math.max(0, attempt - 1);
  return Math.round(base * (0.75 + Math.random() * 0.5));
}
async function rateLimitReason(response, settled) {
  let body;
  try {
    body = await response.clone().json();
  } catch {
    body = void 0;
  }
  const fields = body ?? {};
  for (const value of [fields.errorReason, fields.error, fields.code, fields.reason, settled?.errorReason]) {
    if (typeof value === "string" && value) return value;
  }
  return void 0;
}
function creationFailure(err) {
  const detail = err instanceof Error ? err.message : String(err);
  if (err instanceof Error && err.name === "UntrustedOperatorError") return { reason: "untrusted_operator", detail };
  if (detail.includes("serverSignedChannelsPolicy maxDeposit")) return { reason: "deposit_over_cap", detail };
  if (detail.includes("has a pending request")) return { reason: "channel_pending", detail };
  if (detail.includes("requires the optional peer dependency")) return { reason: "peer_dependency_missing", detail };
  return { reason: "payment_creation_failed", detail };
}
function logValue(value) {
  const text = String(value);
  return /^[\w.:/@+-]+$/.test(text) ? text : JSON.stringify(text);
}
function formatEvent(event) {
  const fields = [
    ["event", event.type],
    ["reason", event.reason],
    ["status", event.status],
    ["errorReason", event.errorReason],
    ["retryAfterMs", event.retryAfterMs],
    ["attempt", event.attempt],
    ["wallet", event.wallet],
    ["next", event.type === "fallback" ? "exact" : event.type === "backoff" ? "retry" : event.type === "unresolved" ? "raise" : void 0],
    ["detail", event.detail]
  ];
  const parts = fields.filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${k}=${logValue(v)}`);
  return `[@blockrun/llm] batch-settlement ${parts.join(" ")}`;
}
var realSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
var sleep2 = realSleep;
var warnedNoSizer = false;
function channelKeyOf(requirements) {
  const accept = requirements;
  const extra = accept?.extra ?? {};
  if (typeof extra.feePayer !== "string" || typeof extra.withdrawDelay !== "number") return void 0;
  return [
    accept.network,
    accept.asset,
    accept.payTo,
    extra.feePayer,
    extra.withdrawDelay,
    extra.receiverAuthorizer ?? "",
    extra.voucherSigner ?? "client",
    extra.operator ?? ""
  ].join(":");
}
function capOutstandingEscrow(scheme, wallet) {
  const target = scheme;
  const original = target.resolveDepositAmount;
  if (typeof original !== "function") {
    if (!warnedNoSizer) {
      warnedNoSizer = true;
      console.error(
        "[@blockrun/llm] batch-settlement: this @x402/svm does not expose resolveDepositAmount; maxDeposit caps lifetime deposits"
      );
    }
    return;
  }
  target.resolveDepositAmount = function(requirements, requestAmount, needed, context, trust, existingDeposit) {
    const topUp = wallet.topUp;
    const widened = topUp && existingDeposit > 0n && existingDeposit === topUp.deposit && channelKeyOf(requirements) === topUp.key && typeof trust?.maxDeposit === "bigint" ? { ...trust, maxDeposit: trust.maxDeposit + topUp.settled } : trust;
    return original.call(this, requirements, requestAmount, needed, context, widened, existingDeposit);
  };
}
var payScope = new import_async_hooks.AsyncLocalStorage();
function registerHookDispatchers(client) {
  client.onBeforePaymentCreation(async (context) => {
    for (const hook of payScope.getStore()?.hooks.before ?? []) {
      const result = await hook(context);
      if (result && typeof result === "object" && result.abort) return result;
    }
    return void 0;
  });
  client.onAfterPaymentCreation(async (context) => {
    const scope = payScope.getStore();
    for (const hook of scope?.hooks.after ?? []) {
      try {
        await hook(context);
      } catch (error) {
        if (scope && !scope.afterError) scope.afterError = { error };
      }
    }
  });
  client.onPaymentCreationFailure(async (context) => {
    await runFailureHooks(payScope.getStore(), context);
    return void 0;
  });
}
async function runFailureHooks(scope, context) {
  for (const hook of scope?.hooks.failure ?? []) {
    try {
      await hook(context);
    } catch {
    }
  }
}
var RPC_SCOPE_STORE = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-rpc-scope/v1");
var RPC_HEADER_HOOK = /* @__PURE__ */ Symbol.for("@blockrun/llm/batch-rpc-headers/v1");
var sharedGlobal = globalThis;
var rpcScope = sharedGlobal[RPC_SCOPE_STORE] ??= new import_async_hooks.AsyncLocalStorage();
function requestUrl(input) {
  return typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
}
function installRpcHeaderHook() {
  const current = globalThis.fetch;
  if (current[RPC_HEADER_HOOK]) return;
  const hooked = (input, init) => {
    const scope = rpcScope.getStore();
    if (!scope || requestUrl(input) !== scope.url) return current(input, init);
    const headers = new Headers(input instanceof Request ? input.headers : void 0);
    new Headers(scope.headers).forEach((value, name) => headers.set(name, value));
    new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
    return current(input, { ...init, headers, redirect: "error" });
  };
  hooked[RPC_HEADER_HOOK] = true;
  globalThis.fetch = hooked;
}
function addResyncTarget(wallet, target) {
  const existing = wallet.resyncs.get(target.key);
  if (!existing) {
    wallet.resyncs.set(target.key, target);
    return;
  }
  wallet.resyncs.set(target.key, {
    ...existing,
    channelConfig: target.channelConfig,
    cumulative: maxOf(existing.cumulative, target.cumulative) ?? 0n,
    expectDeposit: maxOf(existing.expectDeposit, target.expectDeposit),
    knownDeposit: maxOf(existing.knownDeposit, target.knownDeposit),
    anchorHeight: existing.anchorHeight === void 0 || target.anchorHeight === void 0 ? existing.anchorHeight ?? target.anchorHeight : Math.max(existing.anchorHeight, target.anchorHeight)
  });
}
function intentTarget(intent) {
  return {
    key: intent.key,
    channelId: intent.channelId,
    channelConfig: intent.channelConfig,
    cumulative: parseAtomic(intent.cumulative),
    expectDeposit: parseAtomic(intent.expectDeposit),
    ...intent.knownDeposit !== void 0 ? { knownDeposit: parseAtomic(intent.knownDeposit) } : {},
    ...intent.anchorHeight !== void 0 ? { anchorHeight: intent.anchorHeight } : {},
    reason: "orphaned_deposit"
  };
}
var SolanaBatchPayer = class {
  constructor(init) {
    this.init = init;
    init.configureClient?.({
      onBeforePaymentCreation: (hook) => void this.hooks.before.push(hook),
      onAfterPaymentCreation: (hook) => void this.hooks.after.push(hook),
      onPaymentCreationFailure: (hook) => void this.hooks.failure.push(hook)
    });
    const { operators, rateLimit, inDoubt } = init.options;
    if (!Array.isArray(operators) || operators.length === 0 || operators.some((o) => typeof o !== "string" || !o)) {
      throw new Error("batch.operators must list at least one base58 operator public key");
    }
    this.maxAttempts = rateLimit?.maxAttempts ?? DEFAULT_BATCH_RATE_LIMIT.maxAttempts;
    this.maxWaitMs = rateLimit?.maxWaitMs ?? DEFAULT_BATCH_RATE_LIMIT.maxWaitMs;
    if (!Number.isInteger(this.maxAttempts) || this.maxAttempts < 1) {
      throw new Error("batch.rateLimit.maxAttempts must be an integer >= 1");
    }
    if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs < 0) {
      throw new Error("batch.rateLimit.maxWaitMs must be a finite number of ms >= 0");
    }
    this.inDoubtMaxWaitMs = inDoubt?.maxWaitMs ?? DEFAULT_BATCH_IN_DOUBT.maxWaitMs;
    if (!Number.isFinite(this.inDoubtMaxWaitMs) || this.inDoubtMaxWaitMs < 0) {
      throw new Error("batch.inDoubt.maxWaitMs must be a finite number of ms >= 0");
    }
    this.onLocked = init.options.onLocked ?? "exact";
    if (this.onLocked !== "exact" && this.onLocked !== "wait" && this.onLocked !== "throw") {
      throw new Error('batch.onLocked must be "exact", "wait" or "throw"');
    }
    this.lockWaitMs = init.options.lockWaitMs ?? DEFAULT_BATCH_LOCK_WAIT_MS;
    if (!Number.isFinite(this.lockWaitMs) || this.lockWaitMs < 0) {
      throw new Error("batch.lockWaitMs must be a finite number of ms >= 0");
    }
  }
  init;
  state;
  maxAttempts;
  maxWaitMs;
  inDoubtMaxWaitMs;
  onLocked;
  lockWaitMs;
  counters = {
    fallbacks: 0,
    fallbacksByReason: {},
    backoffs: 0,
    retries: 0,
    recoveries: 0,
    resyncs: 0,
    unresolved: 0,
    unresolvedByReason: {}
  };
  hooks = { before: [], after: [], failure: [] };
  /** A copy of this client's batch counters. */
  stats() {
    return {
      ...this.counters,
      fallbacksByReason: { ...this.counters.fallbacksByReason },
      unresolvedByReason: { ...this.counters.unresolvedByReason }
    };
  }
  async storeFile() {
    const { channelStore } = this.init.options;
    if (channelStore === false) return void 0;
    return path5.resolve(channelStore ?? path5.join(paths().dir, "solana-batch", `${await this.init.address()}.json`));
  }
  /**
   * This wallet's shared batch state, or why this client cannot use batch:
   * another client in this process holds the wallet with a different trust
   * configuration, or another live process owns its channel file.
   */
  wallet() {
    this.state ??= (async () => {
      const address = await this.init.address();
      const file = await this.storeFile();
      const { operators, maxDeposit } = this.init.options;
      const headers = Object.entries(this.init.rpcHeaders ?? {}).sort(([a], [b]) => a.localeCompare(b));
      const config = JSON.stringify([[...operators].sort(), String(maxDeposit ?? ""), file ?? null, this.init.rpcUrl, headers]);
      const existing = registry2.wallets.get(address);
      if (existing) {
        return existing.config === config ? existing : {
          reason: "wallet_config_conflict",
          detail: "another client in this process uses this wallet with different batch options"
        };
      }
      if (file) {
        const owner = lockChannelFile(file);
        if (owner !== void 0) {
          return {
            reason: "channel_store_locked",
            ownerPid: owner,
            detail: owner === process.pid ? `channel store ${file} is in use by another copy of @blockrun/llm in this process (or by a process that had this pid and died: remove ${file}.lock if nothing uses it)` : `channel store ${file} is in use by process ${owner}`
          };
        }
      }
      const resyncs = /* @__PURE__ */ new Map();
      const base = file ? new FileChannelStorage(file) : new MemoryChannelStorage();
      const book = new ChannelBook(base, (target) => addResyncTarget(created, target));
      const intents = file ? FileIntentJournal.beside(file) : new MemoryIntentJournal();
      let journaled;
      try {
        if (file) intents.adoptLegacy(FileIntentJournal.legacyBeside(file), address);
        journaled = intents.list();
      } catch (err) {
        return {
          reason: "deposit_journal_unreadable",
          detail: `cannot read ${intents.file}: ${err instanceof Error ? err.message : String(err)}`
        };
      }
      const created = {
        config,
        busy: false,
        cooldownUntil: 0,
        active: 0,
        ...file ? { file } : {},
        book,
        resyncs,
        intents,
        closes: 0
      };
      for (const intent of journaled) addResyncTarget(created, intentTarget(intent));
      registry2.wallets.set(address, created);
      return created;
    })();
    const state = this.state;
    state.then(
      (result) => {
        if ("reason" in result && result.reason !== "wallet_config_conflict" && this.state === state) this.state = void 0;
      },
      () => {
        if (this.state === state) this.state = void 0;
      }
    );
    return state;
  }
  build(wallet) {
    wallet.client ??= (async () => {
      const [core, svm, kit] = await Promise.all([
        load("@x402/core", () => import("@x402/core/client")),
        load("@x402/svm", () => import("@x402/svm/batch-settlement/client")),
        load("@solana/kit", () => import("@solana/kit"))
      ]);
      const signer = await kit.createKeyPairSignerFromBytes(await this.init.secretKey());
      const scheme = new svm.BatchSvmScheme(signer, {
        rpcUrl: this.init.rpcUrl,
        serverSignedChannelsPolicy: {
          allowedOperators: this.init.options.operators,
          maxDeposit: this.init.options.maxDeposit ?? svm.DEFAULT_SERVER_SIGNED_MAX_DEPOSIT
        },
        // Always a store, memory included: a record the SDK repairs from the
        // chain survives the scheme being rebuilt to pick it up.
        channelStorage: wallet.book
      });
      capOutstandingEscrow(scheme, wallet);
      const client = new core.x402Client().register("solana:*", scheme).registerPolicy(scheme.paymentPolicy);
      client.setSpendControls({ maxAmountPerPayment: false });
      registerHookDispatchers(client);
      const raw = new core.x402HTTPClient(client);
      const http = {
        getPaymentSettleResponse: (getHeader) => raw.getPaymentSettleResponse(getHeader),
        createPaymentPayload: (paymentRequired) => this.withRpcHeaders(() => raw.createPaymentPayload(paymentRequired)),
        encodePaymentSignatureHeader: (payload) => raw.encodePaymentSignatureHeader(payload),
        processPaymentResult: (payload, getHeader, status2) => this.withRpcHeaders(() => raw.processPaymentResult(payload, getHeader, status2))
      };
      const loader = scheme.loadChannel;
      return {
        http,
        refund: (url, requirements) => this.withRpcHeaders(() => scheme.refund(url, { requirements })),
        load: async (key) => {
          if (typeof loader === "function") await loader.call(scheme, key);
        }
      };
    })();
    wallet.client.catch(() => {
      wallet.client = void 0;
    });
    return wallet.client;
  }
  /** Run a scheme call so its RPC requests carry this client's `rpcHeaders`. */
  withRpcHeaders(call) {
    const headers = this.init.rpcHeaders;
    if (!headers || Object.keys(headers).length === 0) return call();
    installRpcHeaderHook();
    return rpcScope.run({ url: this.init.rpcUrl, headers }, call);
  }
  /**
   * Drop everything this wallet knows about one channel, on disk and in
   * memory, leaving its other channels' records alone. The scheme is rebuilt
   * on the next call, without the closed channel in its memory.
   */
  async forget(wallet, key) {
    wallet.client = void 0;
    wallet.resyncs.delete(key);
    await wallet.book.forget(key);
    this.forgetIntent(wallet, key);
  }
  /**
   * Stop trusting the record of the channel a payload paid into: the next
   * payment re-reads that channel from the chain first. The scheme has
   * already restored (or, for an open, dropped) its record; the book still
   * knows the channel and the cumulative it last confirmed.
   */
  distrust(wallet, sent, reason, charged = 0n) {
    const { payload } = sent;
    const channelId = payloadChannelId(payload);
    const known = channelId ? wallet.book.find(channelId) : void 0;
    if (!channelId || !known) {
      wallet.client = void 0;
      let intent;
      try {
        intent = sent.key ? wallet.intents.list().find((candidate) => candidate.key === sent.key) : void 0;
      } catch {
        intent = void 0;
      }
      if (intent) addResyncTarget(wallet, { ...intentTarget(intent), reason });
      return;
    }
    const { key, record } = known;
    const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
    const confirmed = hasConfirmed ? parseAtomic(record.chargedCumulativeAmount) : 0n;
    const expectDeposit = sent.expectDeposit ?? this.depositExpectation(wallet, payload).expectDeposit;
    addResyncTarget(wallet, {
      key,
      channelId,
      channelConfig: record.channelConfig,
      cumulative: confirmed + charged,
      ...hasConfirmed ? { knownDeposit: parseAtomic(record.deposit) } : {},
      ...expectDeposit !== void 0 ? { expectDeposit } : {},
      reason
    });
  }
  /**
   * The channel's total deposit if a deposit payload landed: the confirmed
   * deposit plus this one (for an open, nothing plus this one). Undefined for
   * a payment without a deposit, or a channel the SDK has no record of.
   */
  depositExpectation(wallet, payload) {
    if (payload.payload?.type !== "deposit") return {};
    const channelId = payloadChannelId(payload);
    const record = channelId ? wallet.book.find(channelId)?.record : void 0;
    if (!record) return {};
    const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
    return { expectDeposit: (hasConfirmed ? parseAtomic(record.deposit) : 0n) + parseAtomic(payload.payload.deposit?.amount) };
  }
  /**
   * Re-read every distrusted channel from the chain, at `finalized`
   * commitment, and rewrite its record: the on-chain deposit, and the highest
   * of the cumulative the SDK can vouch for, the cumulative the stored record
   * already confirmed, and the on-chain settled amount. A closing or closed
   * channel is dropped. The scheme is rebuilt to load the repaired record.
   *
   * The stored cumulative is never moved backwards. A deposit's intent can
   * outlive the receipt that reconciled it (the process died, or the journal
   * could not be cleared, after the scheme saved the new state), so its
   * cumulative may predate vouchers the scheme has since confirmed; and
   * vouchers are redeemed on-chain asynchronously, so `settled` can lag too.
   * Rewriting the record with either would leave every later voucher
   * unreconcilable.
   *
   * A deposit in doubt is settled by the chain alone:
   * - landed: the finalized channel holds at least the deposit it would
   *   bring (and at least what the record already claimed);
   * - never landed: the finalized chain is {@link LANDING_MARGIN_BLOCKS}
   *   past the target's anchor height, and a read at least that recent still
   *   does not show it. An open's record is then dropped; a top-up's channel
   *   is rewritten with the deposit it really has.
   * Until one of those holds, it is pending. This settles only the deposit:
   * whether the gateway charged the payment that carried it is never read
   * from the chain.
   *
   * @throws ResyncError when the chain cannot be read, or a deposit may still
   *   land: the caller pays exact and the re-read is tried again next call.
   *   Nothing opens or tops up a channel while any re-read is pending.
   */
  async resync(wallet) {
    for (const target of [...wallet.resyncs.values()]) {
      const failed = (err) => err instanceof ResyncError ? err : err instanceof ChannelUnreadableError ? new ResyncError("channel_unreadable", err.message) : err instanceof ChannelLayoutError ? new ResyncError("channel_unreadable", `channel ${target.channelId}: ${err.message}`) : new ResyncError(
        "channel_resync_failed",
        `could not read channel ${target.channelId} from the chain: ${err instanceof Error ? err.message : String(err)}`
      );
      let channel;
      let expired = false;
      let stored;
      try {
        channel = await this.readOwnChannel(target);
        const required = maxOf(target.expectDeposit, target.knownDeposit) ?? 0n;
        if (channel === void 0 || channel.open && channel.deposit < required) {
          if (target.anchorHeight === void 0) {
            target.anchorHeight = rpcInteger(
              await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
              "getBlockHeight"
            );
            try {
              const intent = wallet.intents.list().find((candidate) => candidate.key === target.key);
              if (intent) wallet.intents.put({ ...intent, anchorHeight: target.anchorHeight });
            } catch {
            }
          }
          const epoch = await rpcRequest(this.init.rpcUrl, "getEpochInfo", [{ commitment: "finalized" }], this.init.rpcHeaders);
          const finalizedHeight = rpcInteger(epoch.blockHeight, "getEpochInfo blockHeight");
          const finalizedSlot = rpcInteger(epoch.absoluteSlot, "getEpochInfo absoluteSlot");
          if (finalizedHeight <= target.anchorHeight + LANDING_MARGIN_BLOCKS) {
            throw new ResyncError(
              "channel_resync_pending",
              channel === void 0 ? `channel ${target.channelId} is not finalized on chain, and its deposit may still land` : `channel ${target.channelId} holds ${channel.deposit} finalized, short of the ${required} it should hold, and a deposit may still land`
            );
          }
          channel = await this.readOwnChannel(target, finalizedSlot);
          expired = true;
        }
        stored = await wallet.book.base.get(target.key);
      } catch (err) {
        throw failed(err);
      }
      let outcome;
      if (typeof stored?.channelId === "string" && stored.channelId !== target.channelId) {
        outcome = `the record under its key now names channel ${stored.channelId}; left untouched`;
      } else if (channel === void 0) {
        await wallet.book.delete(target.key);
        outcome = "no such channel at finalized commitment, past its deposit's last valid block; record dropped";
      } else if (!channel.open) {
        await wallet.book.delete(target.key);
        outcome = "channel is closing or closed; record dropped";
      } else {
        const cumulative = maxOf(maxOf(target.cumulative, channel.settled), confirmedCumulative(stored, target.channelId)) ?? 0n;
        await wallet.book.set(target.key, {
          channelConfig: target.channelConfig,
          channelId: target.channelId,
          chargedCumulativeAmount: cumulative.toString(),
          deposit: channel.deposit.toString()
        });
        outcome = `record rewritten from finalized chain: deposit=${channel.deposit} settled=${channel.settled} cumulative=${cumulative}` + (expired ? " (the deposit in doubt never landed)" : "");
      }
      wallet.resyncs.delete(target.key);
      this.forgetIntent(wallet, target.key);
      wallet.client = void 0;
      await this.report({ type: "resync", reason: target.reason, detail: `channel ${target.channelId}: ${outcome}` });
    }
  }
  /**
   * A distrusted channel's account at `finalized` commitment, checked to be
   * this wallet's channel for the record's operator and mint.
   *
   * @throws ResyncError (`channel_unreadable`) for any other payment channel.
   */
  async readOwnChannel(target, minContextSlot) {
    const channel = await readChannelAccount(this.init.rpcUrl, target.channelId, this.init.rpcHeaders, { minContextSlot });
    if (channel !== void 0 && (channel.payer !== await this.init.address() || channel.authorizedSigner !== target.channelConfig.payerAuthorizer || channel.mint !== target.channelConfig.token)) {
      throw new ResyncError("channel_unreadable", `account ${target.channelId} is a payment channel for another payer, operator or mint`);
    }
    return channel;
  }
  /**
   * Make one event observable three ways: a stderr line (every time, never
   * deduplicated), this client's counters, and the caller's `onEvent`.
   */
  async report(event) {
    let wallet;
    try {
      wallet = await this.init.address();
    } catch {
      wallet = "unknown";
    }
    const full = { ...event, wallet, at: Date.now() };
    if (full.type === "fallback") {
      this.counters.fallbacks += 1;
      this.counters.fallbacksByReason[full.reason] = (this.counters.fallbacksByReason[full.reason] ?? 0) + 1;
    } else if (full.type === "backoff") {
      this.counters.backoffs += 1;
    } else if (full.type === "recovered") {
      this.counters.recoveries += 1;
    } else if (full.type === "unresolved") {
      this.counters.unresolved += 1;
      this.counters.unresolvedByReason[full.reason] = (this.counters.unresolvedByReason[full.reason] ?? 0) + 1;
    } else {
      this.counters.resyncs += 1;
    }
    console.error(formatEvent(full));
    const failed = (err) => console.error(
      `[@blockrun/llm] batch-settlement onEvent callback threw: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
    );
    try {
      const result = this.init.options.onEvent?.(full);
      if (result && typeof result.then === "function") {
        result.then(void 0, failed);
      }
    } catch (err) {
      failed(err);
    }
  }
  /** Report a fallback and tell the caller to pay exact. */
  async fallback(event) {
    await this.report({ type: "fallback", ...event });
    return { kind: "fallback", reason: event.reason };
  }
  /**
   * Pay one 402 with batch-settlement, or say why the caller should use exact.
   *
   * `send` replays the original request with the payment headers. Every send
   * goes through {@link sendOnce}, the one place an answer is classified as
   * charged, not charged, or in doubt:
   *
   * - charged: the 2xx is returned (`paid`);
   * - not charged (the gateway's explicit answer to a first send): a 429
   *   whose receipt proves nothing was broadcast is backed off
   *   (`Retry-After`, else 1s, 2s, 4s with jitter) and paid again with a new
   *   payment built from a fresh 402 (`rechallenge`: the original's
   *   `recentBlockhash` / `recentSlot` can be stale by then), within
   *   `rateLimit.maxAttempts` / `maxWaitMs`, then with exact; any other is
   *   paid with exact (`fallback`). A fresh challenge served with a 2xx is
   *   the call's result (`served`, nothing paid); one that fails otherwise
   *   is the call's error (`failed`, `"unpaid"`), never an exact payment
   *   against the stale challenge;
   * - in doubt: ends only with a definitive success receipt from a replay
   *   of the identical payment ({@link resolveInDoubt}: the one replay a
   *   receipt-less 429 is allowed, or the replays of a payment carrying a
   *   payment identifier while the gateway says one can succeed, within
   *   `inDoubt.maxWaitMs`), or a thrown {@link BatchPaymentUnresolvedError}.
   *   No new authorization, no new deposit, no exact payment and no fallback
   *   model is ever paid for a call in doubt.
   *
   * The wallet stays in cooldown after a 429, so its other calls wait
   * instead of sending opens of their own. After a deposit the facilitator
   * refused before broadcasting it (a 402 with a failed `batch_*` admission
   * receipt), its calls do not wait: they pay exact at once
   * (`admission_refused`) until the refusal cooldown ends.
   *
   * @param closesAtStart - {@link closeFence}, taken before the call's first
   *   (unpaid) request. If a close completed since, the call pays exact
   *   (`closed_during_call`): it started before the close, so it must not
   *   open a channel the caller believes is closed.
   */
  async pay(paymentRequired, send, rechallenge, closesAtStart) {
    const unavailable = batchUnavailable(paymentRequired);
    if (unavailable) return this.fallback(unavailable);
    let wallet;
    let waitedMs = 0;
    try {
      wallet = await this.wallet();
      if ("reason" in wallet && wallet.reason === "channel_store_locked" && this.onLocked === "wait") {
        const started = Date.now();
        wallet = await this.waitForLock(wallet);
        waitedMs = Date.now() - started;
      }
    } catch (err) {
      return this.fallback(creationFailure(err));
    }
    if ("reason" in wallet) {
      if (wallet.reason === "channel_store_locked" && this.onLocked !== "exact") {
        return this.lockedOut(wallet.detail, wallet.ownerPid, waitedMs);
      }
      return this.fallback({ reason: wallet.reason, detail: wallet.detail });
    }
    if (closesAtStart !== void 0 && wallet.closes !== closesAtStart) {
      return this.fallback({
        reason: "closed_during_call",
        detail: "closeBatchChannel() closed this wallet's channel after this call started; it opens no channel"
      });
    }
    wallet.active += 1;
    try {
      return await payScope.run(
        { hooks: this.hooks },
        () => this.payWith(wallet, paymentRequired, send, rechallenge, closesAtStart ?? wallet.closes)
      );
    } finally {
      wallet.active -= 1;
    }
  }
  /**
   * `onLocked: "wait"`: try the wallet's channel store again every
   * {@link LOCK_POLL_MS} until its owner lets go of it or `lockWaitMs` runs
   * out. Reported once as a `backoff` event (`channel_store_locked`).
   */
  async waitForLock(refusal) {
    const deadline = Date.now() + this.lockWaitMs;
    let current = refusal;
    if (this.lockWaitMs > 0) {
      await this.report({
        type: "backoff",
        reason: "channel_store_locked",
        retryAfterMs: this.lockWaitMs,
        detail: refusal.detail
      });
    }
    while ("reason" in current && current.reason === "channel_store_locked") {
      const left = deadline - Date.now();
      if (left <= 0) break;
      await sleep2(Math.min(LOCK_POLL_MS, left));
      current = await this.wallet();
    }
    return current;
  }
  /** `onLocked` other than `"exact"`: the call's `"unpaid"` error, nothing signed. */
  async lockedOut(detail, ownerPid, waitedMs) {
    const error = new BatchChannelLockedError({
      wallet: await this.init.address(),
      detail,
      waitedMs,
      ...ownerPid !== void 0 ? { ownerPid } : {}
    });
    return { kind: "failed", error };
  }
  /**
   * The close fence for a call about to send its first request: how many
   * closes this wallet has completed so far, to hand to {@link pay}.
   * `close()` refuses while a call is inside `pay()`, but a call still
   * waiting for its 402 is not there yet; the fence covers that window.
   * Undefined when the wallet cannot be named (no fence then; `pay()`
   * reports why batch is unavailable).
   */
  async closeFence() {
    try {
      return registry2.wallets.get(await this.init.address())?.closes ?? 0;
    } catch {
      return void 0;
    }
  }
  /** {@link pay}, for a wallet this client may use. */
  async payWith(wallet, paymentRequired, send, rechallenge, fence) {
    let current = paymentRequired;
    const fallback = async (event) => {
      const result = await this.fallback(event);
      return current === paymentRequired ? result : { ...result, paymentRequired: current };
    };
    const budget = { attempt: 1, waited: 0 };
    let waitedUntil = 0;
    let backedOff;
    let holding = false;
    let sent;
    const release = () => {
      if (holding) wallet.busy = false;
      holding = false;
    };
    try {
      for (; ; ) {
        const now = Date.now();
        if ((wallet.refusedUntil ?? 0) > now) {
          return fallback({
            reason: "admission_refused",
            errorReason: wallet.refusedFor,
            retryAfterMs: (wallet.refusedUntil ?? 0) - now,
            attempt: budget.attempt,
            detail: `the facilitator refused this wallet's last batch deposit (${wallet.refusedFor ?? "unknown"}); batch is skipped until the refusal cooldown ends`
          });
        }
        if (wallet.cooldownUntil > now && wallet.cooldownUntil !== waitedUntil) {
          const wait = wallet.cooldownUntil - now;
          if (budget.waited + wait > this.maxWaitMs) {
            return fallback({
              reason: "rate_limited",
              retryAfterMs: wait,
              attempt: budget.attempt,
              detail: "wallet is in a 429 cooldown longer than batch.rateLimit.maxWaitMs allows"
            });
          }
          await this.report({ type: "backoff", reason: "cooldown", retryAfterMs: wait, attempt: budget.attempt });
          backedOff ??= "cooldown";
          waitedUntil = wallet.cooldownUntil;
          budget.waited += wait;
          await sleep2(wait);
          continue;
        }
        if (budget.attempt > 1 || budget.waited > 0) {
          let fresh;
          try {
            fresh = await rechallenge();
          } catch (err) {
            return { kind: "failed", error: withDisposition(err instanceof Error ? err : new Error(String(err)), "unpaid") };
          }
          if (fresh.kind === "served") {
            await this.report({
              type: "recovered",
              reason: "served_unpaid_on_rechallenge",
              status: fresh.response.status,
              attempt: budget.attempt,
              detail: "the fresh challenge after the wait was served without a payment; nothing was paid"
            });
            return { kind: "served", response: fresh.response };
          }
          current = fresh.paymentRequired;
          const unavailable = batchUnavailable(current);
          if (unavailable) return fallback({ ...unavailable, attempt: budget.attempt });
        }
        if (wallet.closes !== fence) {
          return fallback({
            reason: "closed_during_call",
            attempt: budget.attempt,
            detail: "closeBatchChannel() closed this wallet's channel after this call started; it opens no channel"
          });
        }
        if (wallet.busy) return fallback({ reason: "channel_busy", attempt: budget.attempt });
        if (wallet.file && !holdsChannelFile(wallet.file)) {
          const detail = `this process no longer holds the lock on ${wallet.file}; another owner may be paying into its channels`;
          if (this.onLocked !== "exact") return this.lockedOut(detail, void 0, 0);
          return fallback({ reason: "channel_store_locked", attempt: budget.attempt, detail });
        }
        wallet.busy = true;
        holding = true;
        let built;
        try {
          built = await this.buildPayment(wallet, current);
        } catch (err) {
          release();
          if (err instanceof ResyncError) return fallback({ reason: err.reason, detail: err.message, attempt: budget.attempt });
          return fallback({ ...creationFailure(err), attempt: budget.attempt });
        }
        if (built.kind !== "authorization") {
          try {
            this.journalDeposit(wallet, built);
          } catch (err) {
            await this.settle(built.http, built.payload, () => null, 0);
            release();
            return fallback({
              reason: "deposit_journal_failed",
              attempt: budget.attempt,
              detail: err instanceof Error ? err.message : String(err)
            });
          }
        }
        sent = built;
        const outcome = await this.sendOnce(wallet, built, send, backedOff === "rate_limited", false);
        if (outcome.kind === "charged") {
          if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
          sent = void 0;
          release();
          if (backedOff) await this.report({ type: "recovered", reason: backedOff, attempt: budget.attempt });
          return { kind: "paid", response: outcome.response, chargedUsd: outcome.chargedUsd };
        }
        if (outcome.kind === "not_charged") {
          if (built.kind !== "authorization") {
            if (outcome.depositSafe) this.forgetIntent(wallet, built.key);
            else this.distrust(wallet, built, "deposit_refused");
            if (outcome.refusedForMs !== void 0) {
              const until = Date.now() + outcome.refusedForMs;
              if (until > (wallet.refusedUntil ?? 0)) {
                wallet.refusedUntil = until;
                wallet.refusedFor = outcome.errorReason;
              }
            }
          } else if (built.key && outcome.reason === "payment_required" && outcome.refusedForMs === void 0) {
            await this.recheckRefusedChannel(wallet, built.key, built.reclaimAnchor);
          }
          sent = void 0;
          release();
          if (!outcome.rateLimited) {
            const { kind: _kind, rateLimited: _limited, depositSafe: _safe, refusedForMs, ...event } = outcome;
            return fallback({
              ...event,
              ...refusedForMs !== void 0 && built.kind !== "authorization" ? { retryAfterMs: refusedForMs } : {},
              attempt: budget.attempt
            });
          }
          const wait = this.coolDown(wallet, outcome.rateLimited.retryAfterMs ?? backoffDelay(budget.attempt));
          if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
            return fallback({
              reason: "rate_limited",
              status: 429,
              errorReason: outcome.errorReason,
              retryAfterMs: wait,
              attempt: budget.attempt,
              detail: budget.attempt >= this.maxAttempts ? "batch.rateLimit.maxAttempts reached" : "Retry-After exceeds what is left of batch.rateLimit.maxWaitMs"
            });
          }
          await this.report({
            type: "backoff",
            reason: "rate_limited",
            status: 429,
            errorReason: outcome.errorReason,
            retryAfterMs: wait,
            attempt: budget.attempt
          });
          backedOff = "rate_limited";
          waitedUntil = wallet.cooldownUntil;
          budget.waited += wait;
          await sleep2(wait);
          budget.attempt += 1;
          continue;
        }
        if (outcome.kind === "cancelled") {
          sent = void 0;
          release();
          await this.report({
            type: "recovered",
            reason: "batch_cancelled",
            status: outcome.status,
            errorReason: "batch_cancelled",
            attempt: budget.attempt,
            detail: "the gateway cancelled the payment: nothing was charged, and its error is raised unpaid"
          });
          return { kind: "failed", error: outcome.error };
        }
        const resolved = await this.resolveInDoubt(wallet, built, outcome, send, budget);
        if (resolved.kind === "charged") {
          if (built.kind !== "authorization" && !(built.key && wallet.resyncs.has(built.key))) this.forgetIntent(wallet, built.key);
          sent = void 0;
          release();
          await this.report({ type: "recovered", reason: outcome.status === 429 ? "rate_limited" : "in_doubt", attempt: budget.attempt });
          return { kind: "paid", response: resolved.response, chargedUsd: resolved.chargedUsd };
        }
        return await this.raiseUnresolved(wallet, built, resolved, budget.attempt);
      }
    } catch (err) {
      if (err instanceof Error && err.name === "BatchPaymentUnresolvedError") throw err;
      if (sent) {
        try {
          return await this.raiseUnresolved(
            wallet,
            sent,
            {
              kind: "in_doubt",
              reason: "outcome_unknown",
              replayable: false,
              settled: false,
              detail: `an error after the payment was sent: ${err instanceof Error ? err.message : String(err)}`,
              cause: err
            },
            budget.attempt
          );
        } catch (raised) {
          throw withDisposition(raised, "paid-or-in-doubt");
        }
      }
      return fallback({ ...creationFailure(err), attempt: budget.attempt });
    } finally {
      release();
    }
  }
  /**
   * Put the wallet in a 429 cooldown for `delay` ms (`Retry-After`, or the
   * default backoff), and return how long this call would have to wait.
   *
   * The shared cooldown is capped at `rateLimit.maxWaitMs`, so one header,
   * however long, never keeps the wallet's calls off batch for longer than
   * the retry budget. This call still weighs the full delay: one it cannot
   * wait out within its budget is not waited at all, and the caller pays
   * exact (or raises a payment in doubt) at once.
   */
  coolDown(wallet, delay) {
    const limitedAt = Date.now();
    wallet.cooldownUntil = Math.max(wallet.cooldownUntil, limitedAt + Math.min(delay, this.maxWaitMs));
    return Math.max(delay, wallet.cooldownUntil - limitedAt);
  }
  /**
   * Try to end a payment in doubt with the gateway's own word.
   *
   * The one source of evidence this SDK accepts is a definitive success
   * receipt on a byte-identical replay of the payment. Any other answer to a
   * replay (a 402 or `duplicate_settlement`, which mean the original reached
   * the gateway; a 409; another 429; a 5xx; a timeout) leaves it in doubt.
   *
   * - A payment carrying a payment identifier is replayed for as long as
   *   the gateway says a later replay can succeed ({@link replayInDoubt}).
   * - Any other payment gets a replay only after a 429 without a receipt:
   *   one, after its backoff (the owner's bounded safety attempt), within
   *   `rateLimit`. Every other in-doubt outcome is never replayed.
   *
   * Chain state is not evidence here: it can settle a deposit (see
   * `resync`), never whether the gateway charged.
   *
   * @returns the charged replay, or the doubt to raise.
   */
  async resolveInDoubt(wallet, sent, doubt, send, budget) {
    if (hasPaymentIdentifier(sent.payload)) return this.replayInDoubt(wallet, sent, doubt, send, budget.attempt);
    if (!doubt.replayable) return doubt;
    const wait = this.coolDown(wallet, doubt.retryAfterMs ?? backoffDelay(budget.attempt));
    if (budget.attempt >= this.maxAttempts || budget.waited + wait > this.maxWaitMs) {
      return {
        ...doubt,
        detail: `${doubt.detail}; batch.rateLimit leaves no room for its one replay`
      };
    }
    await this.report({
      type: "backoff",
      reason: "rate_limited",
      status: 429,
      errorReason: doubt.errorReason,
      retryAfterMs: wait,
      attempt: budget.attempt
    });
    budget.waited += wait;
    await sleep2(wait);
    budget.attempt += 1;
    return replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
  }
  /**
   * Replay a payment in doubt that carries a payment identifier until the
   * gateway's answer settles it.
   *
   * A gateway that declares `payment-identifier` stores each paid response
   * under its identifier and answers a byte-identical replay of a completed
   * request with it, the original receipt included; while the original is
   * still running, it answers 409 with a `Retry-After`. So the identical
   * payment (the same `PAYMENT-SIGNATURE` header and the same body) is
   * replayed for as long as the last answer says a later one can succeed
   * ({@link replayMayResolve}), after {@link inDoubtWait} each time, as long
   * as the wait fits in `inDoubt.maxWaitMs`, counted from the first doubt:
   * every wait (as long as it really took) and every replay's round trip. A
   * replay already sent is not cut short. These replays do not count
   * against `rateLimit`; a 429 among them still puts the wallet in cooldown.
   *
   * Every answer that does not settle it is held back from the scheme
   * ({@link InDoubt.held}) and handed over only once no replay follows, so
   * the scheme rolls the payment back once, or takes the receipt that answer
   * carries. That is the last answer, unless an earlier one carried a
   * success receipt and the later ones do not: a success receipt is the
   * gateway's word that it charged, and handing the scheme a receipt-less
   * refusal of a replay instead would roll a real charge back and desync the
   * channel's cumulative.
   *
   * @returns the charged replay, or the doubt to raise.
   */
  async replayInDoubt(wallet, sent, first, send, attempt) {
    let doubt = first;
    let held = first.held;
    let heldSucceeded = held !== void 0 && receiptSucceeded(sent.http, held);
    let replays = 0;
    let spent = 0;
    const stop = async (note) => {
      if (!doubt.settled && held) {
        const answer = held;
        await this.settle(sent.http, sent.payload, (name) => answer.headers.get(name), answer.status);
      }
      const notes = [replays > 0 ? `${replays} replay${replays === 1 ? "" : "s"} did not resolve it` : "", note ?? ""];
      return {
        ...doubt,
        settled: doubt.settled || held !== void 0,
        held: void 0,
        firstReason: first.reason,
        detail: [doubt.detail, ...notes].filter(Boolean).join("; ")
      };
    };
    for (; ; ) {
      if (!replayMayResolve(doubt)) {
        return stop(doubt.held?.status === 409 ? "the gateway sent no Retry-After, so no replay can resolve it" : void 0);
      }
      const wait = inDoubtWait(doubt, replays + 1);
      const status2 = doubt.held?.status;
      if (status2 === 429) this.coolDown(wallet, (doubt.held && parseRetryAfter(doubt.held.headers.get("retry-after"))) ?? wait);
      if (spent + wait > this.inDoubtMaxWaitMs) {
        return stop(`batch.inDoubt.maxWaitMs (${this.inDoubtMaxWaitMs} ms) leaves no room for ${replays > 0 ? "another replay" : "a replay"}`);
      }
      await this.report({
        type: "backoff",
        reason: status2 === 429 ? "rate_limited" : "in_doubt",
        status: status2,
        errorReason: doubt.errorReason,
        retryAfterMs: wait,
        attempt,
        detail: `replay ${replays + 1} of the payment in doubt`
      });
      const sleptFrom = Date.now();
      await sleep2(wait);
      spent += Math.max(wait, Date.now() - sleptFrom);
      replays += 1;
      const startedAt = Date.now();
      const replayed = replayDoubt(await this.sendOnce(wallet, sent, send, true, true));
      spent += Math.max(0, Date.now() - startedAt);
      if (replayed.kind === "charged") return replayed;
      doubt = replayed;
      if (doubt.held) {
        const succeeded = receiptSucceeded(sent.http, doubt.held);
        if (succeeded || !heldSucceeded) {
          held = doubt.held;
          heldSucceeded = succeeded;
        }
      } else if (doubt.settled) {
        held = void 0;
        heldSucceeded = false;
      }
    }
  }
  /**
   * Raise a payment in doubt: release the scheme's hold on it (the channel
   * goes back to its confirmed state), put a deposit's channel in doubt so
   * it is re-read from the chain before anything pays into it again, report
   * the `unresolved` event, and throw {@link BatchPaymentUnresolvedError}.
   */
  async raiseUnresolved(wallet, sent, doubt, attempt) {
    if (!doubt.settled) await this.settle(sent.http, sent.payload, () => null, 0);
    if (sent.kind !== "authorization") {
      const reason = doubt.firstReason ?? doubt.reason;
      this.distrust(
        wallet,
        sent,
        reason === "no_response" ? "deposit_unanswered" : reason === "replay_unresolved" ? "deposit_rate_limited" : "deposit_failed"
      );
    }
    await this.report({
      type: "unresolved",
      reason: doubt.reason,
      status: doubt.status,
      errorReason: doubt.errorReason,
      attempt,
      detail: `${doubt.detail}; it may have been charged, so it is not paid again`
    });
    throw new BatchPaymentUnresolvedError({
      reason: doubt.reason,
      wallet: await this.init.address(),
      requestId: sent.payload.payload?.authorization?.requestId,
      channelId: payloadChannelId(sent.payload),
      payloadKind: sent.kind,
      status: doubt.status,
      detail: doubt.detail,
      cause: doubt.cause
    });
  }
  /** Build a new batch payment for a 402: the payload, its headers and its kind. */
  async buildPayment(wallet, paymentRequired) {
    const acceptKey = channelKeyOf(trustedBatchAccept(paymentRequired, this.init.options.operators));
    let reclaimAnchor;
    try {
      const stored = acceptKey ? await wallet.book.base.get(acceptKey) : void 0;
      reclaimAnchor = stored ? savedReclaimAnchor(stored) : void 0;
    } catch {
    }
    const { http, payload } = await this.createPayload(wallet, paymentRequired);
    const scope = payScope.getStore();
    if (scope?.afterError) {
      const { error } = scope.afterError;
      scope.afterError = void 0;
      await this.settle(http, payload, () => null, 0);
      await runFailureHooks(scope, {
        paymentRequired,
        selectedRequirements: payload.accepted,
        error
      });
      throw error;
    }
    const headers = http.encodePaymentSignatureHeader(payload);
    const channelId = payloadChannelId(payload);
    const known = channelId ? wallet.book.find(channelId) : void 0;
    let kind = "authorization";
    if (payload.payload?.type === "deposit") {
      const record = known?.record;
      kind = record && (record.pending === void 0 || record.hasConfirmedState) ? "top-up" : "open";
    }
    return {
      http,
      payload,
      headers,
      kind,
      key: known?.key,
      ...reclaimAnchor !== void 0 && known?.key === acceptKey ? { reclaimAnchor } : {},
      sentAt: Date.now(),
      sends: 0
    };
  }
  /**
   * Write a deposit's intent to the wallet's journal before it is sent
   * (with the file store: the file and its directory fsynced).
   *
   * @throws when it cannot be recorded; the deposit must then not be sent.
   */
  journalDeposit(wallet, sent) {
    const channelId = payloadChannelId(sent.payload);
    const record = channelId ? wallet.book.find(channelId)?.record : void 0;
    const { expectDeposit } = this.depositExpectation(wallet, sent.payload);
    if (!sent.key || !channelId || !record || expectDeposit === void 0 || sent.kind === "authorization") {
      throw new Error("the deposit's channel record is missing");
    }
    const hasConfirmed = record.pending === void 0 || record.hasConfirmedState;
    sent.expectDeposit = expectDeposit;
    wallet.intents.put({
      key: sent.key,
      channelId,
      channelConfig: record.channelConfig,
      requestId: sent.payload.payload?.authorization?.requestId,
      kind: sent.kind,
      cumulative: hasConfirmed ? String(parseAtomic(record.chargedCumulativeAmount)) : "0",
      expectDeposit: expectDeposit.toString(),
      ...hasConfirmed ? { knownDeposit: String(parseAtomic(record.deposit)) } : {},
      at: Date.now()
    });
  }
  /**
   * Remove a deposit's intent once it is reconciled. A failure is logged,
   * not thrown: the intent then outlives its deposit, and after a restart
   * the chain re-read settles it again.
   */
  forgetIntent(wallet, key) {
    if (!key) return;
    try {
      wallet.intents.remove(key);
    } catch (err) {
      console.error(
        `[@blockrun/llm] batch-settlement could not clear a deposit intent: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
      );
    }
  }
  /**
   * THE choke point: send a batch payment once and classify the answer.
   *
   * The only caller of `send` for a batch payment, and the only place its
   * outcome is decided (see {@link SendOutcome}). Every exception once the
   * send has started is `in_doubt`, whatever its type or `cause.code`: the
   * request may have reached the gateway. "Not charged" is only ever minted
   * here, and only for a first send.
   *
   * @param retry - count this send in `retries` (it follows this call's own 429).
   * @param replay - this is a replay of a payment in doubt: only a 2xx with
   *   a success receipt counts, everything else stays in doubt.
   */
  async sendOnce(wallet, sent, send, retry, replay) {
    if (retry) this.counters.retries += 1;
    sent.sends += 1;
    let response;
    try {
      response = await send(sent.headers);
    } catch (err) {
      return {
        kind: "in_doubt",
        reason: replay ? "replay_unresolved" : "no_response",
        replayable: false,
        settled: false,
        unanswered: true,
        detail: `${replay ? "the replay" : "the payment"} got no answer (${err instanceof Error ? `${err.name}: ${err.message}` : String(err)})`,
        cause: err
      };
    }
    try {
      return await this.classify(wallet, sent, response, replay);
    } catch (err) {
      return {
        kind: "in_doubt",
        reason: replay ? "replay_unresolved" : "outcome_unknown",
        replayable: false,
        settled: false,
        status: response.status,
        detail: `the answer could not be classified (${err instanceof Error ? err.message : String(err)})`,
        cause: err
      };
    }
  }
  /** {@link sendOnce}'s classifier, for an HTTP answer. */
  async classify(wallet, sent, response, replay) {
    const { http, payload } = sent;
    const getHeader = (name) => response.headers.get(name);
    const receipted = Boolean(getHeader("PAYMENT-RESPONSE") || getHeader("X-PAYMENT-RESPONSE"));
    const status2 = response.status;
    const hold = hasPaymentIdentifier(payload) && (!response.ok || replay && peekReceipt(http, getHeader)?.success !== true);
    if (status2 === 429 && !receipted) {
      return {
        kind: "in_doubt",
        reason: "replay_unresolved",
        replayable: !replay,
        settled: false,
        status: status2,
        retryAfterMs: parseRetryAfter(getHeader("retry-after")),
        errorReason: await rateLimitReason(response, void 0),
        detail: replay ? "the replay was answered 429 without a receipt" : "a 429 without a receipt",
        ...hold ? { held: response } : {}
      };
    }
    const outcome = await this.classifyAnswer(wallet, sent, response, replay, hold);
    if (!hold) return outcome;
    if (outcome.kind === "in_doubt") return { ...outcome, settled: false, held: response };
    await this.settle(http, payload, getHeader, status2);
    return outcome;
  }
  /**
   * {@link classify}, past a receipt-less 429. With `hold`, the answer is not
   * handed to the scheme: its receipt is only read.
   */
  async classifyAnswer(wallet, sent, response, replay, hold) {
    const { http, payload } = sent;
    const getHeader = (name) => response.headers.get(name);
    const status2 = response.status;
    const channelId = payloadChannelId(payload);
    const before = channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0;
    const reconciled = hold ? { settleResponse: peekReceipt(http, getHeader), ok: false } : await this.settle(http, payload, getHeader, status2);
    const receipt = reconciled.settleResponse;
    const definitive = receipt?.success === true && reconciled.ok && recordAdvanced(before, channelId ? confirmedState(wallet.book.find(channelId)?.record) : void 0, payload, receipt);
    if (!response.ok && !replay && sent.kind === "authorization" && cancelledReceipt(receipt)) {
      return { kind: "cancelled", status: status2, error: await cancelledError(response) };
    }
    if (response.ok && (!replay || definitive)) {
      if (!definitive) {
        this.distrust(wallet, sent, receipt ? "receipt_unreconciled" : "receipt_missing", rebuiltCharge(receipt, payload));
      }
      return { kind: "charged", response, chargedUsd: chargedUsd(() => http.getPaymentSettleResponse(getHeader)) };
    }
    if (replay) {
      return {
        kind: "in_doubt",
        reason: "replay_unresolved",
        replayable: false,
        settled: true,
        status: status2,
        errorReason: status2 === 429 ? await rateLimitReason(response, receipt) : receipt?.errorReason,
        detail: `the replay was answered ${status2} with ${describeReceipt(receipt)}`,
        cause: await afterPaymentError(response)
      };
    }
    if (status2 === 429) {
      if (provesNothingBroadcast(receipt)) {
        return {
          kind: "not_charged",
          reason: "rate_limited",
          status: status2,
          errorReason: await rateLimitReason(response, receipt),
          rateLimited: { retryAfterMs: parseRetryAfter(getHeader("retry-after")) },
          // The facilitator's explicit failed receipt names no transaction: the
          // deposit was never broadcast (see provesNothingBroadcast).
          depositSafe: true
        };
      }
      return {
        kind: "in_doubt",
        reason: "ambiguous_rate_limit",
        replayable: false,
        settled: true,
        status: status2,
        errorReason: await rateLimitReason(response, receipt),
        detail: `a 429 whose ${describeReceipt(receipt)} does not prove nothing was broadcast`,
        cause: await afterPaymentError(response)
      };
    }
    if (cleanRefusalReceipt(receipt)) {
      if (status2 === 402) {
        const duplicate = await duplicateAnswer(response);
        if (duplicate) {
          return {
            kind: "in_doubt",
            reason: "duplicate_settlement",
            replayable: false,
            settled: true,
            status: status2,
            errorReason: duplicate,
            detail: `the payment was answered 402 ${duplicate}: another copy of this request reached the gateway`,
            cause: await afterPaymentError(response)
          };
        }
        const refused = provesNothingBroadcast(receipt);
        return {
          kind: "not_charged",
          reason: "payment_required",
          status: status2,
          errorReason: receipt?.errorReason,
          depositSafe: refused,
          ...refused ? { refusedForMs: refusalCooldownMs(receipt?.errorReason, parseRetryAfter(getHeader("retry-after"))) } : {}
        };
      }
      const refusal = await batchRefusal(response);
      if (refusal) {
        return { kind: "not_charged", reason: refusal, status: status2, errorReason: refusal, depositSafe: PRE_REQUEST_REFUSALS.has(refusal) };
      }
    }
    return {
      kind: "in_doubt",
      reason: "outcome_unknown",
      replayable: false,
      settled: true,
      status: status2,
      errorReason: receipt?.errorReason,
      detail: `the payment was answered ${status2} with ${describeReceipt(receipt)}`,
      cause: await afterPaymentError(response)
    };
  }
  /**
   * Build the payment payload, re-reading any distrusted channel from the
   * chain first. A stored record the book refuses to hand over (a deposit a
   * dead process never heard back about), or one a top-up's read found
   * unusable ({@link prepareTopUp}), is re-read and the payload rebuilt
   * once.
   */
  async createPayload(wallet, paymentRequired) {
    for (let pass = 0; ; pass += 1) {
      await this.resync(wallet);
      const http = (await this.build(wallet)).http;
      try {
        await this.prepareTopUp(wallet, paymentRequired);
        const required = await this.withDepositBlockhash(wallet, withoutClientSignedBatch(paymentRequired));
        return { http, payload: await http.createPaymentPayload(withPaymentIdentifier(required)) };
      } catch (err) {
        if (pass > 0 || !isResyncRequired(err) || wallet.resyncs.size === 0) throw err;
      } finally {
        wallet.topUp = void 0;
      }
    }
  }
  /**
   * The 402 to build this payment from: with a finalized blockhash
   * ({@link withDepositBlockhash}) when the payment will likely carry a
   * deposit (no usable record for the trusted accept's channel, so an open,
   * or a top-up {@link prepareTopUp} just sized). An authorization signs no
   * transaction and costs no RPC read. Best effort: a failed read leaves the
   * 402 as it is, and `@x402/svm` fetches its own blockhash.
   */
  async withDepositBlockhash(wallet, paymentRequired) {
    const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
    const key = accept ? channelKeyOf(accept) : void 0;
    if (!accept || accept.extra?.recentBlockhash) return paymentRequired;
    let deposit = wallet.topUp !== void 0;
    if (!deposit) {
      try {
        const record = key ? await wallet.book.base.get(key) : void 0;
        deposit = !record || typeof record.channelId !== "string" || record.pending !== void 0;
      } catch {
        deposit = true;
      }
    }
    if (!deposit) return paymentRequired;
    try {
      return withDepositBlockhash(paymentRequired, await finalizedBlockhash(this.init.rpcUrl, this.init.rpcHeaders));
    } catch {
      return paymentRequired;
    }
  }
  /**
   * Queue a re-read of a stored channel whose account is gone at finalized
   * and at confirmed commitment (closed, its account reclaimed), anchored at
   * the SDK's own `getBlockHeight` at confirmed. The anchor is saved in the
   * record (`reclaimAnchorHeight`), so a short-lived process finishes the
   * wait instead of starting it over; a failed read leaves resync to take it,
   * and a failed save only costs that. resync pays exact until finality is
   * LANDING_MARGIN_BLOCKS past the anchor, then drops the record if the
   * finalized chain still has no account (a lagging read finds the channel
   * and rewrites the record, without the anchor).
   */
  async queueReclaimed(wallet, key, record, channelId, savedAnchor) {
    let anchorHeight = savedReclaimAnchor(record) ?? savedAnchor;
    if (anchorHeight === void 0) {
      try {
        anchorHeight = rpcInteger(
          await rpcRequest(this.init.rpcUrl, "getBlockHeight", [{ commitment: "confirmed" }], this.init.rpcHeaders),
          "getBlockHeight"
        );
      } catch {
      }
    }
    if (anchorHeight !== void 0 && savedReclaimAnchor(record) !== anchorHeight) {
      try {
        await wallet.book.set(key, { ...record, reclaimAnchorHeight: anchorHeight });
      } catch {
      }
    }
    addResyncTarget(wallet, {
      key,
      channelId,
      channelConfig: record.channelConfig,
      cumulative: parseAtomic(record.chargedCumulativeAmount),
      knownDeposit: parseAtomic(record.deposit),
      ...anchorHeight !== void 0 ? { anchorHeight } : {},
      reason: "channel_unusable"
    });
  }
  /**
   * After the gateway refused an authorization with a plain 402 (no receipt,
   * not an admission refusal), check the channel it paid into. That is how
   * the gateway answers an authorization into a channel the facilitator
   * closed (its verify rejects it), and a channel with deposit to spare is
   * otherwise never read again: every call would sign another authorization
   * into it and pay exact. A channel the finalized chain shows closing,
   * closed or not this wallet's is re-read through resync (record dropped,
   * or kept and paid exact); one gone at every commitment is queued as
   * {@link queueReclaimed}; an open one of this wallet's, or one only at
   * confirmed (not finalized yet), is left as it is. A queued re-read is run
   * at once. Best effort: a failed read changes nothing, and the next
   * refusal checks again.
   */
  async recheckRefusedChannel(wallet, key, savedAnchor) {
    try {
      const record = await wallet.book.get(key);
      if (!record || record.pending || typeof record.channelId !== "string" || wallet.resyncs.has(key)) return;
      const channelId = record.channelId;
      const channel = await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders);
      if (channel !== void 0) {
        if (channel.open && channel.payer === await this.init.address() && channel.authorizedSigner === record.channelConfig.payerAuthorizer && channel.mint === record.channelConfig.token) {
          return;
        }
        addResyncTarget(wallet, {
          key,
          channelId,
          channelConfig: record.channelConfig,
          cumulative: parseAtomic(record.chargedCumulativeAmount),
          knownDeposit: parseAtomic(record.deposit),
          reason: "channel_unusable"
        });
      } else {
        if (await readChannelAccount(this.init.rpcUrl, channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0) return;
        await this.queueReclaimed(wallet, key, record, channelId, savedAnchor);
      }
      await this.resync(wallet);
    } catch {
    }
  }
  /**
   * When this payment will top up the wallet's channel, read how much of its
   * deposit is already settled on-chain (at `finalized` commitment, so never
   * more than will stay settled), so `maxDeposit` caps the escrow still at
   * stake rather than every deposit the channel ever took.
   *
   * Only a top-up costs this read (a channel's balance decides it, from the
   * stored record). If the read fails, nothing settled is assumed, which is
   * the stricter, lifetime cap. If the chain shows a larger deposit than the
   * record, the record is behind (a deposit it never heard about landed):
   * sizing a top-up from it could exceed `maxDeposit`, so this call pays
   * exact and the channel is re-read before the next payment. If
   * `@x402/svm`'s channel layout is not the one this SDK decodes, or the
   * account at the channel's address cannot be read as a channel, the call
   * pays exact (`channel_unreadable`), record kept. If
   * the finalized channel is closing or closed, or is not this wallet's for
   * the record's operator and mint, no top-up is sized from the record: it
   * is re-read at once (`channel_unusable`), see {@link createPayload}. So is
   * a channel with no account at finalized or at confirmed commitment (closed
   * and reclaimed); resync drops its record only once finality is past its
   * own anchor. One that shows only at confirmed is not finalized yet: the
   * lifetime cap applies.
   */
  async prepareTopUp(wallet, paymentRequired) {
    wallet.topUp = void 0;
    const accept = trustedBatchAccept(paymentRequired, this.init.options.operators);
    if (!accept) return;
    const key = channelKeyOf(accept);
    const record = key ? await wallet.book.get(key) : void 0;
    if (!key || !record || record.pending || typeof record.channelId !== "string") return;
    const deposit = parseAtomic(record.deposit);
    if (parseAtomic(record.chargedCumulativeAmount) + parseAtomic(accept.amount) <= deposit) return;
    let channel;
    let unfinalized = false;
    try {
      channel = await readChannelAccount(this.init.rpcUrl, record.channelId, this.init.rpcHeaders);
      if (channel === void 0) {
        unfinalized = await readChannelAccount(this.init.rpcUrl, record.channelId, this.init.rpcHeaders, { commitment: "confirmed" }) !== void 0;
      }
    } catch (err) {
      if (err instanceof ChannelLayoutError) {
        throw new ResyncError("channel_unreadable", `channel ${record.channelId}: ${err.message}; no top-up is sized from it`);
      }
      if (err instanceof ChannelUnreadableError) {
        throw new ResyncError("channel_unreadable", `${err.message}; no top-up is signed into it`);
      }
      throw new ResyncError(
        "channel_resync_failed",
        `could not read channel ${record.channelId} before topping it up: ${err instanceof Error ? err.message : String(err)}`
      );
    }
    if (channel === void 0 && unfinalized) return;
    if (channel === void 0) {
      await this.queueReclaimed(wallet, key, record, record.channelId);
      throw new ChannelResyncRequiredError(record.channelId);
    }
    if (!channel.open || channel.payer !== await this.init.address() || channel.authorizedSigner !== record.channelConfig.payerAuthorizer || channel.mint !== record.channelConfig.token) {
      addResyncTarget(wallet, {
        key,
        channelId: record.channelId,
        channelConfig: record.channelConfig,
        cumulative: parseAtomic(record.chargedCumulativeAmount),
        knownDeposit: deposit,
        reason: "channel_unusable"
      });
      throw new ChannelResyncRequiredError(record.channelId);
    }
    if (channel.deposit > deposit) {
      addResyncTarget(wallet, {
        key,
        channelId: record.channelId,
        channelConfig: record.channelConfig,
        cumulative: parseAtomic(record.chargedCumulativeAmount),
        knownDeposit: channel.deposit,
        reason: "deposit_unrecorded"
      });
      throw new ResyncError(
        "channel_resync_pending",
        `channel ${record.channelId} holds ${channel.deposit} on chain, more than the ${deposit} its record knows; it is re-read before any top-up`
      );
    }
    wallet.topUp = { key, deposit, settled: channel.settled };
  }
  /**
   * Hand the gateway's answer to the scheme, which verifies the operator's
   * voucher and advances (or rolls back) the channel's local state.
   *
   * A malformed receipt must not lose a response the caller has been served,
   * so a scheme error is logged rather than thrown.
   */
  async settle(http, payload, getHeader, status2) {
    try {
      return { settleResponse: (await http.processPaymentResult(payload, getHeader, status2)).settleResponse, ok: true };
    } catch (err) {
      console.error(
        `[@blockrun/llm] batch-settlement receipt not reconciled: ${JSON.stringify(err instanceof Error ? err.message : String(err))}`
      );
      return { settleResponse: peekReceipt(http, getHeader), ok: false };
    }
  }
  /**
   * Close the channel the gateway's current challenge names and return its
   * unused escrow to the wallet.
   *
   * The scheme keeps a closed channel in its records, and would go on paying
   * into it, so a successful close forgets that channel. The next batch call
   * finds no open channel on-chain and opens a new one. Any other channel
   * (say, one opened with an operator key that is being rotated out) keeps
   * its record, so it can still be closed without an on-chain scan. A
   * client-signed channel an earlier SDK version opened is closed too, once
   * the trusted server-signed one has no record (see {@link refundTarget}).
   *
   * @param url - any batch-enabled route on the gateway the channel was opened with.
   * @throws BatchCloseDeferredError while a batch call for the wallet is in
   *   flight, or while any of its deposits is in doubt (until a finalized
   *   chain read settles it). Nothing is refunded or forgotten then.
   */
  async close(url) {
    const wallet = await this.wallet();
    if ("reason" in wallet) throw new Error(`batch-settlement unavailable: ${wallet.detail}`);
    if (wallet.busy || wallet.active > 0) {
      throw new BatchCloseDeferredError({
        reason: "call_in_flight",
        wallet: await this.init.address(),
        detail: "a batch call is in flight (it may be waiting out a 429); close the channel when it returns"
      });
    }
    wallet.busy = true;
    try {
      await this.settleDoubtsForClose(wallet);
      const { accept, key } = await this.refundTarget(wallet, url);
      const result = await (await this.prepareClose(wallet, key)).refund(url, accept);
      wallet.closes += 1;
      await this.forget(wallet, key);
      return result;
    } finally {
      wallet.busy = false;
    }
  }
  /**
   * Re-read every distrusted channel before a close. A deposit still in
   * doubt defers the close: refunding (or forgetting) a channel a deposit
   * may yet land on could strand that deposit, and after a restart nothing
   * would remember it. A record re-read that only failed for another reason
   * (an RPC error on a channel with no deposit in doubt) does not.
   */
  async settleDoubtsForClose(wallet) {
    try {
      await this.resync(wallet);
    } catch (err) {
      if (!(err instanceof ResyncError)) throw err;
      const inDoubt = [...wallet.resyncs.values()].filter((target) => target.expectDeposit !== void 0);
      if (inDoubt.length > 0) {
        throw new BatchCloseDeferredError({
          reason: "deposit_in_doubt",
          wallet: await this.init.address(),
          channelIds: inDoubt.map((target) => target.channelId),
          detail: `a deposit on channel ${inDoubt.map((target) => target.channelId).join(", ")} is in doubt (${err.message}); close once a finalized chain read settles it`
        });
      }
    }
  }
  /**
   * The channel a close targets: the one the gateway's refund challenge
   * names, by the scheme's storage key for its trusted server-signed accept
   * (network, asset, receiver, fee payer, withdraw delay, receiver
   * authorizer, operator). Probed with an unpaid GET, as `@x402/svm`'s own
   * refund probes; that accept is then handed to the refund, so upstream
   * does not probe again and pick another accept.
   *
   * A legacy client-signed channel (opened by an earlier SDK version, which
   * could pay a client-signed accept; this one never opens one) must stay
   * refundable. When the trusted server-signed accept has no stored record,
   * or there is none, a client-signed accept in the challenge whose storage
   * key names a stored client-signed record is the target instead, and the
   * refund runs through `@x402/svm`'s client-signed path (a refund voucher
   * signed by the payer at its confirmed cumulative). The signer modes match:
   * a server-signed record is never refunded under a client-signed accept.
   */
  async refundTarget(wallet, url) {
    const probe = await fetch(url, { method: "GET" });
    const header = probe.status === 402 ? probe.headers.get("PAYMENT-REQUIRED") : null;
    if (!header) throw new Error(`refund probe expected a 402 with PAYMENT-REQUIRED from ${url}, got HTTP ${probe.status}`);
    const paymentRequired = parsePaymentRequired(header);
    const trustedAccepts = trustedBatchAccepts(paymentRequired, this.init.options.operators).map((accept) => ({ accept, key: channelKeyOf(accept) })).filter((candidate) => !!candidate.key);
    for (const candidate of trustedAccepts) {
      if (await wallet.book.base.get(candidate.key)) return candidate;
    }
    for (const accept of (paymentRequired.accepts ?? []).filter(clientSignedBatch)) {
      const key = channelKeyOf(accept);
      const record = key ? await wallet.book.base.get(key) : void 0;
      if (key && record && (record.channelConfig?.voucherSigner ?? "client") === "client") return { accept, key };
    }
    const first = trustedAccepts[0];
    if (!first) throw new Error(`${url} offers no batch-settlement accept with a trusted operator to close against`);
    return first;
  }
  /**
   * Get the stored channel `key` ready to refund.
   *
   * `@x402/svm` refunds a server-signed channel only from the scheme's memory,
   * or after an on-chain scan that many RPCs refuse; it never reads the
   * stored record. So the target channel is loaded into a freshly built
   * scheme, and only that one: upstream's refund falls back to the first
   * channel in memory for the same receiver and asset, whatever its operator.
   * A record in doubt (a deposit this process never confirmed, or a pending
   * deposit left by a process that died) is re-read from the chain before it
   * is loaded. If a re-read cannot settle a record, the refund is still
   * attempted and may find the channel by its scan.
   */
  async prepareClose(wallet, key) {
    for (let pass = 0; ; pass += 1) {
      await this.settleDoubtsForClose(wallet);
      wallet.client = void 0;
      const built = await this.build(wallet);
      try {
        await built.load(key);
        return built;
      } catch (err) {
        if (pass > 0 || !isResyncRequired(err)) return built;
      }
    }
  }
};

// src/solana-client.ts
var SOLANA_API_URL = "https://sol.blockrun.ai/api";
var DEFAULT_MAX_TOKENS2 = 1024;
var STALE_BLOCKHASH_RETRY_BACKOFFS_MS = [500, 2e3];
var MAX_PAYMENT_FAILURE_BYTES = 64 * 1024;
var DEFAULT_VIDEO_MODEL = "xai/grok-imagine-video";
var DEFAULT_MUSIC_MODEL = "minimax/music-2.5+";
var DEFAULT_VIDEO_BUDGET_MS = 9e5;
var DEFAULT_MUSIC_BUDGET_MS = 3e5;
var VIDEO_POLL_INTERVAL_MS = 5e3;
var MAX_PAID_POLL_REFUSALS = 4;
var MAX_RETRY_AFTER_MS2 = 12e4;
var MEDIA_SUBMIT_TIMEOUT_MS = 12e4;
function sleep3(ms) {
  return new Promise((resolve2) => setTimeout(resolve2, ms));
}
async function readJsonOrEmpty(response) {
  try {
    const parsed = await response.json();
    return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
  } catch {
    return {};
  }
}
function isPendingPollStatus(status2) {
  return status2 === 202 || status2 === 503 || status2 === 504;
}
function retryAfterMs(response) {
  const raw = response.headers.get("retry-after");
  if (!raw || !/^\d+$/.test(raw.trim())) return void 0;
  return Math.min(Number(raw.trim()) * 1e3, MAX_RETRY_AFTER_MS2);
}
function pollKindFor(url) {
  const path7 = new URL(url).pathname;
  if (path7.includes("/v1/videos")) return "video";
  if (path7.includes("/v1/audio/")) return "audio";
  return "image";
}
var SafeStaleBlockhashError = class extends PaymentError {
  constructor() {
    super("Payment verification used an expired Solana blockhash; retrying with a fresh quote.");
    this.name = "SafeStaleBlockhashError";
  }
};
function normalizePaymentSignal(value) {
  return typeof value === "string" ? value.toLowerCase().replace(/[_\-\s:]/g, "") : "";
}
async function readPaymentFailureBody(response) {
  const reader = response.body?.getReader();
  if (!reader) return "";
  const decoder = new TextDecoder();
  let total = 0;
  let text = "";
  try {
    for (; ; ) {
      const { done, value } = await reader.read();
      if (done) return text + decoder.decode();
      total += value.byteLength;
      if (total > MAX_PAYMENT_FAILURE_BYTES) {
        void reader.cancel();
        return null;
      }
      text += decoder.decode(value, { stream: true });
    }
  } catch {
    return null;
  }
}
async function isSafeStaleBlockhashResponse(response) {
  const length = Number(response.headers.get("content-length") || "0");
  if (Number.isFinite(length) && length > MAX_PAYMENT_FAILURE_BYTES) return false;
  const text = await readPaymentFailureBody(response);
  if (text === null) return false;
  let body;
  try {
    const parsed = JSON.parse(text);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
    body = parsed;
  } catch {
    return false;
  }
  const nested = body.error && typeof body.error === "object" && !Array.isArray(body.error) ? body.error : void 0;
  const errorLabel = typeof body.error === "string" ? body.error : "";
  const code = normalizePaymentSignal(body.code ?? nested?.code);
  const reason = normalizePaymentSignal(body.reason);
  const detail = normalizePaymentSignal(body.invalidMessage);
  const message = normalizePaymentSignal(nested?.message ?? body.message);
  const label = normalizePaymentSignal(errorLabel);
  if (code.includes("settlementfailed") || label.includes("settlementfailed") || message.includes("settlementfailed")) return false;
  const verifyPhase = code === "paymentinvalid" || label.includes("verificationfailed") || message.includes("verificationfailed");
  if (!verifyPhase) return false;
  return code === "paymentblockhashstale" || detail.includes("blockhashnotfound") || detail.includes("blockheightexceeded") || reason === "expiredsignature" || message.includes("expiredsignature");
}
async function waitForStaleRetry(attempt) {
  await new Promise(
    (resolve2) => setTimeout(resolve2, STALE_BLOCKHASH_RETRY_BACKOFFS_MS[attempt])
  );
}
async function unpaidApiError(response) {
  let errorBody;
  try {
    errorBody = await response.json();
  } catch {
    errorBody = { error: "Request failed" };
  }
  return withDisposition(
    new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody)),
    "unpaid"
  );
}
var DEFAULT_SOLANA_RPC_URL = "https://sol.blockrun.ai/api/v1/solana/rpc";
function resolveRpcConfig(rpcUrl, rpcHeaders) {
  const env = typeof process !== "undefined" && process.env ? process.env : {};
  const url = rpcUrl || env.SOLANA_RPC_URL || DEFAULT_SOLANA_RPC_URL;
  let headers;
  if (rpcHeaders) {
    headers = { ...rpcHeaders };
  } else if (env.SOLANA_RPC_HEADERS) {
    try {
      const parsed = JSON.parse(env.SOLANA_RPC_HEADERS);
      if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
        headers = Object.fromEntries(
          Object.entries(parsed).map(([k, v]) => [String(k), String(v)])
        );
      }
    } catch {
    }
  } else if (env.SOLANA_RPC_API_KEY) {
    headers = { "x-api-key": env.SOLANA_RPC_API_KEY };
  }
  return headers ? { url, headers } : { url };
}
var SolanaLLMClient = class {
  static SOLANA_API_URL = SOLANA_API_URL;
  apiAuth;
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  _privateKey;
  get privateKey() {
    return requireWallet(this._privateKey);
  }
  apiUrl;
  rpcUrl;
  rpcHeaders;
  timeout;
  sessionTotalUsd = 0;
  sessionCalls = 0;
  addressCache = null;
  modelPricingCache = null;
  modelPricingPromise = null;
  batchPayer;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const envKey = typeof process !== "undefined" && process.env ? process.env.SOLANA_WALLET_KEY : void 0;
      const privateKey = options.privateKey || envKey;
      if (!privateKey) {
        throw new Error(
          "Private key required. Pass privateKey in options or set SOLANA_WALLET_KEY environment variable."
        );
      }
      this._privateKey = privateKey;
    }
    const apiUrl = this.apiAuth?.apiUrl || options.apiUrl || SOLANA_API_URL;
    validateApiUrl(apiUrl);
    this.apiUrl = apiUrl.replace(/\/$/, "");
    const rpc = resolveRpcConfig(options.rpcUrl, options.rpcHeaders);
    this.rpcUrl = rpc.url;
    this.rpcHeaders = rpc.headers;
    this.timeout = options.timeout || DEFAULT_TIMEOUT;
    if (options.batch && !this.apiAuth) {
      this.batchPayer = new SolanaBatchPayer({
        options: options.batch,
        secretKey: () => solanaKeyToBytes(this.privateKey),
        address: () => this.getWalletAddress(),
        rpcUrl: this.rpcUrl,
        rpcHeaders: this.rpcHeaders
      });
    }
  }
  /**
   * Close this wallet's batch-settlement channel and return its unused escrow.
   *
   * Closes the channel for the operator the gateway's current 402 names; a
   * channel opened with another trusted operator key (during a key rotation)
   * keeps its record. The gateway closes it cooperatively when it can;
   * otherwise this starts a payer-forced close and the escrow comes back
   * after the channel's grace period. Only meaningful with the `batch`
   * option.
   *
   * @throws BatchCloseDeferredError, closing nothing, while a chat call for
   *   this wallet is still in flight (including one waiting out a 429, so a
   *   retry can never reopen a channel you just closed), or while one of its
   *   deposits is in doubt (until a `finalized` chain read shows it landed or
   *   that it no longer can, usually a few minutes after it was sent). Call
   *   it again later.
   */
  async closeBatchChannel() {
    if (!this.batchPayer) throw new Error("closeBatchChannel() requires the `batch` option in wallet mode");
    return this.batchPayer.close(`${this.apiUrl}/v1/chat/completions`);
  }
  /**
   * This client's batch-settlement counters: calls that fell back to `exact`
   * (in total and by reason), 429 backoffs, batch retries, and recoveries.
   * Every one of those is also logged to stderr and passed to `batch.onEvent`.
   * Only meaningful with the `batch` option.
   */
  getBatchStats() {
    if (!this.batchPayer) throw new Error("getBatchStats() requires the `batch` option in wallet mode");
    return this.batchPayer.stats();
  }
  /** Get Solana wallet address (public key in base58). */
  async getWalletAddress() {
    if (!this.addressCache) {
      this.addressCache = await solanaPublicKey(this.privateKey);
    }
    return this.addressCache;
  }
  /** Simple 1-line chat. */
  async chat(model, prompt, options) {
    const messages = [];
    if (options?.system) messages.push({ role: "system", content: options.system });
    messages.push({ role: "user", content: prompt });
    const result = await this.chatCompletion(model, messages, {
      maxTokens: options?.maxTokens,
      temperature: options?.temperature,
      topP: options?.topP,
      search: options?.search,
      searchParameters: options?.searchParameters,
      responseFormat: options?.responseFormat,
      stop: options?.stop,
      fallbackModels: options?.fallbackModels
    });
    return result.choices[0].message.content || "";
  }
  /** Full chat completion (OpenAI-compatible). */
  async chatCompletion(model, messages, options) {
    const routingProfile = routingProfileForModel(model);
    if (routingProfile) {
      return (await this.smartChatCompletion(messages, {
        ...options,
        routingProfile
      })).response;
    }
    validateMaxTokens(options?.maxTokens);
    const buildBody = (candidate) => {
      const body = {
        model: candidate,
        messages,
        max_tokens: options?.maxTokens || DEFAULT_MAX_TOKENS2
      };
      if (options?.temperature !== void 0) body.temperature = options.temperature;
      if (options?.topP !== void 0) body.top_p = options.topP;
      if (options?.searchParameters !== void 0) body.search_parameters = toWireSearchParameters(options.searchParameters);
      else if (options?.search === true) body.search_parameters = { mode: "on" };
      if (options?.tools !== void 0) body.tools = toWireTools(options.tools);
      if (options?.toolChoice !== void 0) body.tool_choice = options.toolChoice;
      if (options?.responseFormat !== void 0) body.response_format = options.responseFormat;
      if (options?.stop !== void 0) body.stop = options.stop;
      return body;
    };
    const chain = [model, ...options?.fallbackModels ?? []];
    let lastError;
    for (let index = 0; index < chain.length; index += 1) {
      const candidate = chain[index];
      try {
        return await this.requestWithPayment("/v1/chat/completions", buildBody(candidate), options?.timeout);
      } catch (error) {
        lastError = error;
        if (!isTransientError(error) || index === chain.length - 1) throw error;
        console.error(`[@blockrun/llm] ${candidate} -> ${chain[index + 1]} (${errSummary(error)})`);
      }
    }
    throw lastError;
  }
  async getModelPricing() {
    if (this.modelPricingCache) return this.modelPricingCache;
    if (this.modelPricingPromise) return this.modelPricingPromise;
    this.modelPricingPromise = (async () => {
      const models = await this.listModels();
      const pricing = /* @__PURE__ */ new Map();
      for (const model of models) {
        const raw = model;
        if (model.available === false) continue;
        const num = (value) => {
          const n = Number(value ?? 0);
          return Number.isFinite(n) ? n : 0;
        };
        const inputPrice = num(model.inputPrice ?? raw.input_price ?? raw.pricing?.input);
        const outputPrice = num(model.outputPrice ?? raw.output_price ?? raw.pricing?.output);
        const flatPrice = num(model.flatPrice ?? raw.flat_price ?? raw.pricing?.flat);
        pricing.set(model.id, {
          inputPrice,
          outputPrice,
          ...(model.billingMode ?? raw.billing_mode) === "flat" && flatPrice > 0 ? { flatPrice } : {}
        });
      }
      registerSearchModels(pricing, models);
      return pricing;
    })();
    try {
      this.modelPricingCache = await this.modelPricingPromise;
      return this.modelPricingCache;
    } finally {
      this.modelPricingPromise = null;
    }
  }
  /** Inspect a Solana route without making or paying for a model call. */
  async route(prompt, options) {
    return routeWithCatalog(
      prompt,
      options?.system,
      options?.maxOutputTokens ?? options?.maxTokens ?? DEFAULT_MAX_TOKENS2,
      await this.getModelPricing(),
      {
        routingProfile: options?.routingProfile,
        requiresStructuredOutput: options?.responseFormat !== void 0,
        search: options?.search,
        searchParameters: options?.searchParameters,
        minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
      }
    );
  }
  /** Smart one-line chat paid on Solana. */
  async smartChat(prompt, options) {
    const decision = await this.route(prompt, options);
    const response = await this.chat(decision.model, prompt, {
      ...options,
      // An explicit caller-supplied chain wins over the routed one.
      fallbackModels: options?.fallbackModels ?? decision.fallbacks
    });
    return { response, model: decision.model, routing: decision };
  }
  /** Smart full message/tool completion paid on Solana. */
  async smartChatCompletion(messages, options = {}) {
    const { prompt, systemPrompt, conversationChars, hasVision } = routingText(messages);
    const decision = routeWithCatalog(
      prompt,
      systemPrompt,
      options.maxOutputTokens ?? options.maxTokens ?? DEFAULT_MAX_TOKENS2,
      await this.getModelPricing(),
      {
        routingProfile: options.routingProfile,
        requiresStructuredOutput: options.responseFormat !== void 0,
        tools: options.tools,
        toolChoice: options.toolChoice,
        search: options.search,
        searchParameters: options.searchParameters,
        conversationChars,
        hasVision,
        minimumPaymentUsd: this.apiAuth ? 0 : SOLANA_MINIMUM_PAYMENT_USD
      }
    );
    const response = await this.chatCompletion(decision.model, messages, {
      maxTokens: options.maxTokens,
      temperature: options.temperature,
      topP: options.topP,
      search: options.search,
      searchParameters: options.searchParameters,
      tools: options.tools,
      toolChoice: options.toolChoice,
      responseFormat: options.responseFormat,
      stop: options.stop,
      timeout: options.timeout,
      // An explicit caller-supplied chain wins over the routed one.
      fallbackModels: options.fallbackModels ?? decision.fallbacks
    });
    response.routing = decision;
    return { response, model: decision.model, routing: decision };
  }
  /**
   * Stream a Server-Sent Events endpoint, paid on Solana.
   *
   * The Solana counterpart to `BlockrunClient.stream`, and the reason it had to
   * exist: a streaming harness cannot use this client at all without it, so
   * "BlockRun supports Solana" stopped being true the moment a caller streamed.
   * `chatCompletion` buffers the whole answer, which is the wrong shape for an
   * agent loop and for anything that shows tokens as they arrive.
   *
   * The handshake is the one the non-streaming paths use — `402`, sign an SPL
   * TransferChecked authorization locally, replay with `PAYMENT-SIGNATURE` —
   * with the same verification-phase re-sign on a stale blockhash. What differs
   * is that the paid response is not read as JSON: it is handed to the SSE
   * reader with its body untouched.
   *
   * A `200` on the first request is returned as-is and settles nothing. That is
   * the free tier (the gateway answers a `billing_mode: "free"` model without a
   * 402 at all) and it is also API-key mode, where billing is on the account
   * rather than on a wallet.
   *
   * Yields each `data:` frame parsed as JSON, and stops at `data: [DONE]`.
   * Malformed frames are skipped rather than thrown — see {@link readSseFrames}.
   *
   * @example
   * for await (const chunk of client.stream<ChatChunk>("/v1/chat/completions", {
   *   model: "deepseek/deepseek-chat",
   *   messages: [{ role: "user", content: "Hi" }],
   *   stream: true,
   * })) {
   *   process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");
   * }
   *
   * @param path - endpoint after the API root; a leading `/api` is tolerated.
   * @param body - JSON request body. Set `stream: true` yourself — this method
   *   does not inject it, because the gateway prices a streaming and a
   *   non-streaming request the same and silently rewriting a caller's body is
   *   how you end up debugging a request you did not send.
   * @returns each decoded SSE frame, in order.
   */
  async *stream(path7, body) {
    const url = this.buildUrl(path7);
    const response = await this.openPaidStream(url, JSON.stringify(body ?? {}));
    yield* readSseFrames(
      response,
      (status2) => new APIError("Stream response has no body", status2, {})
    );
  }
  /**
   * Get to a streaming response, paying for it if the gateway asks.
   *
   * Separate from {@link SolanaLLMClient.stream} because a generator cannot
   * retry cleanly around a `yield`: the stale-blockhash re-sign has to finish
   * before the first frame is handed out, and putting the loop here keeps the
   * payment decision entirely ahead of any output the caller has seen.
   *
   * @param url - resolved endpoint URL.
   * @param requestBody - the serialized body, reused verbatim on the paid retry
   *   so the gateway prices and answers the same request it quoted for.
   * @returns a response whose body has not been read.
   */
  async openPaidStream(url, requestBody) {
    for (let staleRetries = 0; ; ) {
      const response = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
        body: requestBody
      });
      if (response.ok) return response;
      if (response.status !== 402) {
        let errorBody;
        try {
          errorBody = await response.json();
        } catch {
          errorBody = { error: "Request failed" };
        }
        throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
      }
      try {
        const { paymentPayload, costUsd } = await this.signPaymentFrom402(
          url,
          response,
          staleRetries > 0
        );
        const paid = await this.fetchWithTimeout(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "User-Agent": USER_AGENT,
            "PAYMENT-SIGNATURE": paymentPayload
          },
          body: requestBody
        });
        await this.assertPaid(paid);
        this.recordSettlement(costUsd, paid);
        return paid;
      } catch (error) {
        if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
        await waitForStaleRetry(staleRetries++);
        continue;
      }
    }
  }
  /**
   * Resolve an endpoint path against this client's API root.
   *
   * A leading `/api` is stripped for the same reason `BlockrunClient` strips
   * it: the documented paths are written `/api/v1/…` on the website and
   * `/v1/…` in this SDK, and a caller who copies one into the other should get
   * their request rather than a 404.
   * @param path - endpoint path, with or without a leading slash.
   * @returns the absolute URL to call.
   */
  buildUrl(path7) {
    let normalized = path7.startsWith("/") ? path7 : `/${path7}`;
    if (normalized.startsWith("/api/")) normalized = normalized.slice(4);
    return `${this.apiUrl}${normalized}`;
  }
  /** List available models. */
  async listModels() {
    const response = await this.fetchWithTimeout(`${this.apiUrl}/v1/models`, { method: "GET" });
    if (!response.ok) {
      throw new APIError(`Failed to list models: ${response.status}`, response.status);
    }
    const data = await response.json();
    return data.data || [];
  }
  /**
   * Get Solana USDC balance.
   *
   * @returns USDC balance as a float
   */
  async getBalance(options) {
    const usdc_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
    const address = await this.getWalletAddress();
    try {
      const response = await fetch(this.rpcUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json", ...this.rpcHeaders ?? {} },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: 1,
          method: "getTokenAccountsByOwner",
          params: [address, { mint: usdc_mint }, { encoding: "jsonParsed" }]
        })
      });
      const data = await response.json();
      if (options?.strict && (!response.ok || data.error)) {
        throw new Error(`Solana RPC balance lookup failed: ${data.error?.message ?? `HTTP ${response.status}`}`);
      }
      const accounts = data.result?.value || [];
      if (!accounts.length) return 0;
      let total = 0;
      for (const acct of accounts) {
        total += acct.account?.data?.parsed?.info?.tokenAmount?.uiAmount || 0;
      }
      return total;
    } catch (error) {
      if (options?.strict) throw error;
      return 0;
    }
  }
  /** Edit an image using img2img (Solana payment). */
  /**
   * Generate an image on Solana.
   *
   * Slow models answer 202 `{ id, poll_url }`; `requestWithPaymentRaw` follows
   * that to completion, re-signing per poll, and settles once at the end. Fast
   * models answer 200 inline and settle there.
   */
  async image(prompt, options = {}) {
    const body = { prompt, ...options };
    const data = await this.requestWithPaymentRaw("/v1/images/generations", body);
    return data;
  }
  async imageEdit(prompt, image, options) {
    const body = {
      model: options?.model || "openai/gpt-image-2",
      prompt,
      image,
      size: options?.size || "1024x1024",
      n: options?.n || 1
    };
    if (options?.mask !== void 0) body.mask = options.mask;
    const data = await this.requestWithPaymentRaw("/v1/images/image2image", body);
    return data;
  }
  /**
   * Generate a short video on Solana.
   *
   * The POST is verified, not charged. The job is then polled with a wallet
   * signature (no payment) until it finishes, and ONE payment is signed for
   * the finished video; a failed job is never charged. If the gateway does not
   * accept poll signatures for the job, it is polled with a payment per poll
   * instead, which is likewise only charged on completion.
   *
   * @param prompt - text description of the video.
   * @param options - `model` (default `xai/grok-imagine-video`), `budgetMs`
   *   (total wait, default 15 min); any other keys are sent verbatim, so use
   *   the gateway's snake_case names (`duration_seconds`, `image_url`,
   *   `aspect_ratio`, `resolution`, …).
   */
  async video(prompt, options = {}) {
    const { model, budgetMs, ...rest } = options;
    const body = { ...rest, model: model || DEFAULT_VIDEO_MODEL, prompt };
    const data = await this.requestWithPaymentRaw(
      "/v1/videos/generations",
      body,
      { pollBudgetMs: budgetMs ?? DEFAULT_VIDEO_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
    );
    return data;
  }
  /**
   * Generate a music track on Solana. Paid at the POST; a slow track is then
   * polled with a wallet signature, never another payment.
   *
   * @param prompt - music style, mood, or description.
   * @param options - `model` (default `minimax/music-2.5+`), `instrumental`
   *   (default true), `lyrics` (requires `instrumental: false`), `budgetMs`
   *   (total wait, default 5 min); other keys are sent verbatim.
   */
  async music(prompt, options = {}) {
    const { model, instrumental: instrumentalOption, lyrics: rawLyrics, budgetMs, ...rest } = options;
    const instrumental = instrumentalOption ?? true;
    const lyrics = rawLyrics?.trim();
    if (instrumental && lyrics) throw new Error("Cannot specify lyrics when instrumental is true");
    const body = { ...rest, model: model || DEFAULT_MUSIC_MODEL, prompt, instrumental };
    if (lyrics) body.lyrics = lyrics;
    const data = await this.requestWithPaymentRaw(
      "/v1/audio/generations",
      body,
      { pollBudgetMs: budgetMs ?? DEFAULT_MUSIC_BUDGET_MS, submitTimeoutMs: Math.max(this.timeout, MEDIA_SUBMIT_TIMEOUT_MS) }
    );
    return data;
  }
  /** Standalone search (Solana payment). */
  async search(query, options) {
    const body = { query, max_results: options?.maxResults || 10 };
    if (options?.sources !== void 0) body.sources = options.sources;
    if (options?.fromDate !== void 0) body.from_date = options.fromDate;
    if (options?.toDate !== void 0) body.to_date = options.toDate;
    const data = await this.requestWithPaymentRaw("/v1/search", body);
    return data;
  }
  // ── Prediction Markets (Powered by Predexon) ────────────────────────────
  async pm(path7, params) {
    return this.getWithPaymentRaw(`/v1/pm/${path7}`, params);
  }
  async pmQuery(path7, query) {
    return this.requestWithPaymentRaw(`/v1/pm/${path7}`, query);
  }
  // ── Exa Web Search (Powered by Exa) ──────────────────────────────────────
  /**
   * Generic Exa endpoint proxy (POST, Solana payment). Powered by Exa.
   *
   * @param path - Exa endpoint: "search" | "find-similar" | "contents" | "answer"
   * @param body - Request body (see Exa API docs)
   *
   * @example
   * const results = await client.exa("search", { query: "latest AI research", numResults: 5 });
   */
  async exa(path7, body) {
    return this.requestWithPaymentRaw(`/v1/exa/${path7}`, body);
  }
  /**
   * Neural and keyword web search via Exa (Solana payment).
   *
   * @example
   * const results = await client.exaSearch("latest AI papers", { numResults: 5 });
   */
  async exaSearch(query, options) {
    return this.requestWithPaymentRaw("/v1/exa/search", { query, ...options });
  }
  /**
   * Find pages semantically similar to a given URL via Exa (Solana payment).
   *
   * @example
   * const results = await client.exaFindSimilar("https://openai.com/research/gpt-4", { numResults: 5 });
   */
  async exaFindSimilar(url, options) {
    return this.requestWithPaymentRaw("/v1/exa/find-similar", { url, ...options });
  }
  /**
   * Extract full text content from URLs via Exa (Solana payment).
   *
   * @example
   * const data = await client.exaContents(["https://arxiv.org/abs/2303.08774"]);
   */
  async exaContents(urls, options) {
    return this.requestWithPaymentRaw("/v1/exa/contents", { urls, ...options });
  }
  /**
   * AI-generated answer grounded in live web search via Exa (Solana payment).
   *
   * @example
   * const answer = await client.exaAnswer("What is the current state of AI safety research?");
   */
  async exaAnswer(query, options) {
    return this.requestWithPaymentRaw("/v1/exa/answer", { query, ...options });
  }
  // ============================================================
  // DefiLlama (DeFi protocols / TVL / yields / prices)
  // ============================================================
  /**
   * Query DefiLlama DeFi data (GET passthrough). Powered by DefiLlama.
   * for protocols / protocol/{slug} / chains / yields;
   * for prices/{coins}.
   *
   * @param path - e.g. "protocols", "protocol/aave", "chains", "yields",
   *               "prices/coingecko:bitcoin,base:0x..."
   * @param params - Query parameters passed through to DefiLlama
   */
  async defi(path7, params) {
    return this.getWithPaymentRaw(`/v1/defillama/${path7}`, params);
  }
  /** All DeFi protocols with TVL. */
  async defiProtocols() {
    return this.defi("protocols");
  }
  /** Single protocol details + historical TVL. */
  async defiProtocol(slug) {
    return this.defi(`protocol/${slug}`);
  }
  /** Current TVL of every chain. */
  async defiChains() {
    return this.defi("chains");
  }
  /** Yield pools with APY/TVL. */
  async defiYields(params) {
    return this.defi("yields", params);
  }
  /** Token price lookup. Coins like "coingecko:bitcoin" or "{chain}:{address}". */
  async defiPrices(coins) {
    const joined = Array.isArray(coins) ? coins.join(",") : coins;
    return this.defi(`prices/${joined}`);
  }
  // ============================================================
  // 0x DEX (swap quotes + gasless) — free passthrough
  // ============================================================
  /**
   * Query the 0x Swap / Gasless APIs (free — no x402 payment; BlockRun
   * takes an on-chain affiliate fee on executed swaps instead).
   *
   * @param path - "price", "quote", "gasless/price", "gasless/quote",
   *               "gasless/submit" (POST), "gasless/status/{hash}",
   *               "gasless/approval-tokens", "gasless/chains", "swap/chains"
   * @param params - Query params (chainId, sellToken, buyToken, sellAmount, taker, ...)
   * @param body - JSON body — pass to switch to POST (gasless/submit only)
   */
  async dex(path7, params, body) {
    if (body) return this.requestWithPaymentRaw(`/v1/zerox/${path7}`, body);
    return this.getWithPaymentRaw(`/v1/zerox/${path7}`, params);
  }
  /** Indicative Permit2 swap price — no commitment (free). */
  async dexPrice(params) {
    return this.dex("price", params);
  }
  /** Firm Permit2 swap quote with permit2.eip712 + tx data (free). */
  async dexQuote(params) {
    return this.dex("quote", params);
  }
  /** Gasless indicative price quote (free). */
  async dexGaslessPrice(params) {
    return this.dex("gasless/price", params);
  }
  /** Gasless firm quote — returns trade.eip712 to sign (free). */
  async dexGaslessQuote(params) {
    return this.dex("gasless/quote", params);
  }
  /** Submit a signed gasless trade; the 0x relayer pays gas (free). */
  async dexGaslessSubmit(body) {
    return this.dex("gasless/submit", void 0, body);
  }
  /** Poll a gasless trade's status by tradeHash (free). */
  async dexGaslessStatus(tradeHash) {
    return this.dex(`gasless/status/${tradeHash}`);
  }
  /** Chains where the Swap API is supported (free). */
  async dexChains() {
    return this.dex("swap/chains");
  }
  /** Chains where the Gasless API is supported (free). */
  async dexGaslessChains() {
    return this.dex("gasless/chains");
  }
  // ============================================================
  // Modal Sandbox (pay-per-call cloud compute)
  // ============================================================
  /**
   * Call the Modal sandbox compute API (POST passthrough).
   *
   * @param path - "sandbox/create" (CPU / GPU tiers), "sandbox/exec",
   *               "sandbox/status", "sandbox/terminate"
   * @param body - JSON body for the endpoint
   */
  async modal(path7, body) {
    return this.requestWithPaymentRaw(`/v1/modal/${path7}`, body ?? {});
  }
  /** Create a sandboxed compute environment (CPU / GPU tiers). */
  async modalSandboxCreate(body) {
    return this.modal("sandbox/create", body);
  }
  /** Execute a command in a sandbox; returns stdout/stderr. */
  async modalSandboxExec(sandboxId, command, extra) {
    return this.modal("sandbox/exec", { sandbox_id: sandboxId, command, ...extra });
  }
  /** Check a sandbox's status. */
  async modalSandboxStatus(sandboxId) {
    return this.modal("sandbox/status", { sandbox_id: sandboxId });
  }
  /** Terminate a sandbox. */
  async modalSandboxTerminate(sandboxId) {
    return this.modal("sandbox/terminate", { sandbox_id: sandboxId });
  }
  /** Get session spending. */
  getSpending() {
    if (this.apiAuth) throw new Error("Account usage is available at https://user.blockrun.ai/dashboard; getSpending() tracks x402 settlements only.");
    return { totalUsd: this.sessionTotalUsd, calls: this.sessionCalls };
  }
  /** True if using sol.blockrun.ai. */
  isSolana() {
    return this.apiUrl.includes("sol.blockrun.ai");
  }
  /**
   * Chat request with automatic x402 payment.
   *
   * Every error it throws carries a {@link RetryDisposition}, which is what
   * {@link SolanaLLMClient.chatCompletion}'s fallback walk honours: errors of
   * the unpaid first request, its 402 challenge and the signing step are
   * `"unpaid"`; anything after a payment was sent (exact or batch) is
   * `"paid-or-in-doubt"`, and so is any error with an API key once the billed
   * request was sent, except the account API's explicit 4xx refusal.
   */
  async requestWithPayment(endpoint, body, timeoutMs) {
    const url = `${this.apiUrl}${endpoint}`;
    const json = JSON.stringify(body);
    const closesAtStart = await this.batchPayer?.closeFence();
    for (let staleRetries = 0; ; ) {
      const response = await this.sendUnpaid(url, {
        method: "POST",
        headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
        body: json
      }, timeoutMs);
      if (response.status === 402) {
        try {
          let paymentRequired;
          try {
            paymentRequired = await this.readPaymentRequired(response);
          } catch (error) {
            throw withDisposition(error, "unpaid");
          }
          let exactRequired = paymentRequired;
          if (this.batchPayer && staleRetries === 0) {
            const batch = await this.batchPayer.pay(
              paymentRequired,
              (paymentHeaders) => this.fetchWithTimeout(url, {
                method: "POST",
                headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT, ...paymentHeaders },
                body: json
              }, timeoutMs),
              // A fresh, unpaid challenge after a 429 wait: the request itself,
              // sent again without a payment. Every error it throws is unpaid.
              async () => {
                const challenge = await this.sendUnpaid(url, {
                  method: "POST",
                  headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
                  body: json
                }, timeoutMs);
                if (challenge.status === 402) {
                  try {
                    return { kind: "challenge", paymentRequired: await this.readPaymentRequired(challenge) };
                  } catch (error) {
                    throw withDisposition(error, "unpaid");
                  }
                }
                if (challenge.ok) return { kind: "served", response: challenge };
                throw await unpaidApiError(challenge);
              },
              closesAtStart
            );
            if (batch.kind === "paid") {
              this.recordSettlement(batch.chargedUsd, batch.response);
              const paid = await this.readPaidJson(batch.response);
              return withCost(paid, batch.response, batch.chargedUsd, "batch");
            }
            if (batch.kind === "served") return this.withUnpaidCost(await this.readUnpaidJson(batch.response));
            if (batch.kind === "failed") throw batch.error;
            exactRequired = batch.paymentRequired ?? paymentRequired;
          }
          return await this.handlePaymentAndRetry(url, json, exactRequired, staleRetries > 0, timeoutMs);
        } catch (error) {
          if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
          await waitForStaleRetry(staleRetries++);
          continue;
        }
      }
      if (!response.ok) throw await unpaidApiError(response);
      return this.withUnpaidCost(await this.readUnpaidJson(response));
    }
  }
  /**
   * Cost of a chat response served without a payment: 0 with a wallet (a free
   * model). With an API key the call is billed to the account, so costUsd stays
   * undefined (see the dashboard).
   */
  withUnpaidCost(served) {
    if (!this.apiAuth) {
      served.costUsd = 0;
      served.settlement = { scheme: "free", quotedUsd: 0 };
    }
    return served;
  }
  /**
   * Read the JSON body of a 2xx served without a payment: the free tier, or
   * with an API key, where this very request was the billed one.
   */
  async readUnpaidJson(response) {
    try {
      return await response.json();
    } catch (error) {
      throw withDisposition(error, this.apiAuth ? "paid-or-in-doubt" : "unpaid");
    }
  }
  /**
   * Send a request that carries no payment, marking what it throws with its
   * {@link RetryDisposition}: `"unpaid"` with a wallet; with an API key, see
   * {@link accountErrorDisposition} (the request itself is billed).
   */
  async sendUnpaid(url, init, timeoutMs) {
    try {
      return await this.fetchWithTimeout(url, init, timeoutMs);
    } catch (error) {
      throw withDisposition(error, this.apiAuth ? accountErrorDisposition(error) : "unpaid");
    }
  }
  /** Read a paid 2xx response's JSON body. The call was charged, so a failure here is never retried. */
  async readPaidJson(response) {
    try {
      return await response.json();
    } catch (error) {
      throw withDisposition(error, "paid-or-in-doubt");
    }
  }
  /**
   * Follow a 202 `{ id, poll_url }` to completion on a Solana media route
   * (image, music, video).
   *
   * **Polls prove ownership, they do not pay.** A Solana payment is a
   * transaction pinned to a recent blockhash (~60s), so paying every poll of a
   * multi-minute job meant signing a fresh transaction every few seconds. Each
   * poll here instead carries the `x-poll-*` headers: the submitting wallet's
   * ed25519 signature over `blockrun-poll:v1:<kind>:<job id>:<unix seconds>`
   * (see `poll-ownership.ts`). That proof never charges.
   *
   * **Who pays when:**
   * - image / music: the POST already settled (sol settles at submit because the
   *   signed transaction would be dead by completion). The caller recorded the
   *   cost at POST; this loop only fetches the result and never signs a payment.
   *   A batch-billed image job still answers its proof poll with 402: that job
   *   falls back to the per-poll payment below.
   * - video: settles on completion, and a failed job is never charged. A
   *   finished, unpaid job answers the proof poll with 402 `{status:"completed"}`
   *   and a normal x402 challenge; ONE payment is signed from that challenge and
   *   the same poll is re-sent with it. The cost is recorded here, on delivery.
   *   A paid poll the gateway answers 402 (settlement in progress on another
   *   poll, outcome unknown, stale blockhash) is re-polled after its
   *   Retry-After — the gateway's per-job settlement claim makes a later fresh
   *   payment safe — up to `MAX_PAID_POLL_REFUSALS` times.
   *
   * **Fallback, once per job:** if the gateway refuses the proof with its "no
   * such job" answer (video 400, image/music 404) or answers it with a plain
   * payment challenge (a gateway or job that predates poll proofs), the rest of
   * this job is polled the old way — an x-payment signed per poll, which the
   * gateway verifies against the job's payer and does not charge until a video
   * completes. Logged to stderr; never switches back.
   */
  async followSolanaJob(submitBody, budgetMs, kind, intervalMs = kind === "video" ? VIDEO_POLL_INTERVAL_MS : 2e3) {
    const id = submitBody.id;
    const pollPath = submitBody.poll_url;
    if (!id || !pollPath) return submitBody;
    const pollUrl = this.resolvePollUrl(pollPath);
    const jobId = pollJobIdFromUrl(pollUrl);
    const deadline = Date.now() + budgetMs;
    let lastStatus = submitBody.status || "queued";
    let ownershipPolls = true;
    let waitMs = intervalMs;
    let paidRefusalsLeft = MAX_PAID_POLL_REFUSALS;
    let videoCostUsd;
    let settleAttempted = false;
    const fallBack = (why) => {
      ownershipPolls = false;
      waitMs = 0;
      console.warn(
        `[@blockrun/llm] ${kind} job ${id}: ${why}; falling back to x-payment polling for this job`
      );
    };
    const deliver = (data, response) => {
      if (kind === "video") {
        if (videoCostUsd !== void 0) this.recordSettlement(videoCostUsd, response);
        const txHash = response.headers.get("x-payment-receipt");
        if (txHash) data.txHash = txHash;
      }
      return data;
    };
    const failIfFailed = (data, status2) => {
      if (lastStatus === "failed") {
        throw new APIError(
          `Upstream job failed: ${data.error || "unknown"}`,
          status2,
          sanitizeErrorResponse(data)
        );
      }
    };
    while (Date.now() < deadline) {
      if (waitMs > 0) await sleep3(Math.min(waitMs, Math.max(0, deadline - Date.now())));
      waitMs = intervalMs;
      let challenge;
      if (ownershipPolls) {
        const proof = signPollOwnership(await solanaKeyToBytes(this.privateKey), kind, jobId);
        const polled = await this.fetchWithTimeout(pollUrl, {
          method: "GET",
          headers: { "User-Agent": USER_AGENT, ...proof }
        });
        const data2 = await readJsonOrEmpty(polled);
        if (polled.status === 402) {
          if (kind === "video" && data2.status === "completed") {
            lastStatus = "completed";
            settleAttempted = true;
            challenge = new Response(JSON.stringify(data2), { status: 402, headers: polled.headers });
          } else {
            fallBack(
              kind === "image" ? "the gateway asks for a payment on this poll (a batch-billed job, or a gateway without poll proofs)" : "the gateway answered the poll proof with a payment challenge (a gateway without poll proofs)"
            );
            continue;
          }
        } else if (polled.status === (kind === "video" ? 400 : 404)) {
          fallBack(`the gateway refused the poll proof (HTTP ${polled.status})`);
          continue;
        } else {
          lastStatus = data2.status || lastStatus;
          failIfFailed(data2, polled.status);
          if (polled.status === 200 && lastStatus === "completed") return deliver(data2, polled);
          if (!isPendingPollStatus(polled.status)) {
            throw new APIError(`Poll failed: ${polled.status}`, polled.status, sanitizeErrorResponse(data2));
          }
          waitMs = retryAfterMs(polled) ?? intervalMs;
          continue;
        }
      }
      if (!challenge) {
        const unpaid = await this.fetchWithTimeout(pollUrl, {
          method: "GET",
          headers: { "User-Agent": USER_AGENT }
        });
        if (unpaid.status === 200) {
          return deliver(await unpaid.json(), unpaid);
        }
        if (unpaid.status !== 402) {
          let errorBody;
          try {
            errorBody = await unpaid.json();
          } catch {
            errorBody = { error: "Poll failed" };
          }
          throw new APIError(`Poll failed: ${unpaid.status}`, unpaid.status, sanitizeErrorResponse(errorBody));
        }
        challenge = unpaid;
      }
      const { paymentPayload, costUsd } = await this.signPaymentFrom402(
        pollUrl,
        challenge,
        true,
        // always a fresh blockhash; the last one is stale by now
        pollUrl
      );
      if (kind === "video") videoCostUsd = costUsd;
      let paid;
      try {
        paid = await this.fetchWithTimeout(pollUrl, {
          method: "GET",
          headers: { "User-Agent": USER_AGENT, "PAYMENT-SIGNATURE": paymentPayload }
        });
      } catch (error) {
        if (kind === "video" && lastStatus === "completed") settleAttempted = true;
        if (--paidRefusalsLeft <= 0) {
          throw withDisposition(
            new PaymentError(
              `Paid poll failed in transit (${error instanceof Error ? error.message : String(error)}). The payment may have settled: resume the job by polling ${pollUrl} with the same wallet before submitting again.`
            ),
            "paid-or-in-doubt"
          );
        }
        continue;
      }
      const data = await readJsonOrEmpty(paid);
      lastStatus = data.status || lastStatus;
      failIfFailed(data, paid.status);
      if (paid.status === 200 && lastStatus === "completed") return deliver(data, paid);
      if (paid.status === 402 && kind === "video") {
        if (data.error === "Payment verification failed" && !await isSafeStaleBlockhashResponse(new Response(JSON.stringify(data), { status: 402 }))) {
          throw new PaymentError(
            `Payment for the finished video was rejected. Check your Solana USDC balance, then resume the job by polling ${pollUrl} with the same wallet.`
          );
        }
        settleAttempted = true;
        if (--paidRefusalsLeft <= 0) {
          throw withDisposition(
            new PaymentError(
              `Payment for the finished video did not settle after ${MAX_PAID_POLL_REFUSALS} attempts (last: ${data.error || "HTTP 402"}). It may still settle: resume the job by polling ${pollUrl} with the same wallet before submitting again.`
            ),
            "paid-or-in-doubt"
          );
        }
        waitMs = retryAfterMs(paid) ?? intervalMs;
        continue;
      }
      if (!isPendingPollStatus(paid.status) && paid.status !== 200) {
        throw new APIError(`Poll failed: ${paid.status}`, paid.status, sanitizeErrorResponse(data));
      }
      waitMs = retryAfterMs(paid) ?? intervalMs;
    }
    const billing = kind === "video" ? settleAttempted ? "A payment for the finished video was sent and may still settle." : "Nothing was charged: a video is charged only when a completed poll is paid." : "The submit was already paid.";
    throw new APIError(
      `Job did not complete within ${Math.round(budgetMs / 1e3)}s (last status: ${lastStatus}). ${billing} Resume by polling the same poll_url with the same wallet before submitting again.`,
      504,
      { id, last_status: lastStatus, poll_url: pollUrl }
    );
  }
  /**
   * Absolute URL for a job's `poll_url`. The gateway returns `/api/v1/...`;
   * {@link buildUrl} drops the `/api` that `apiUrl` already ends with. An
   * absolute poll_url must be on the gateway's own origin: the poll carries a
   * wallet signature (and, for a finished video, a payment).
   */
  resolvePollUrl(pollPath) {
    if (!/^https?:\/\//i.test(pollPath)) return this.buildUrl(pollPath);
    if (new URL(pollPath).origin !== new URL(this.apiUrl).origin) {
      throw new APIError(`poll_url is not on the gateway origin: ${pollPath}`, 502, { poll_url: pollPath });
    }
    return pollPath;
  }
  /**
   * Turn a `402` into a signed Solana payment payload.
   *
   * Extracted because four call sites need it — chat, the raw POST helpers, the
   * raw GET helper, and {@link SolanaLLMClient.stream} — and it had been
   * written out three times before this. That mattered more than ordinary
   * duplication: this is the code that signs a transfer of the caller's USDC,
   * so three copies meant every fix to it had to be applied three times or
   * quietly apply to two thirds of the paths.
   *
   * @param url - the request being paid for.
   * @param response - the gateway's `402`, not yet consumed.
   * @param forceFreshBlockhash - set on a re-sign after a stale-blockhash
   *   rejection, so the retry cannot produce byte-identical transaction bytes.
   * @param resourceFallback - resource URL to claim when the 402 states none.
   * @returns the header value to replay with, and what it will settle for.
   * @throws PaymentError when the 402 carries no usable Solana requirements.
   */
  async signPaymentFrom402(url, response, forceFreshBlockhash, resourceFallback = url) {
    return this.signExactPayment(
      url,
      await this.readPaymentRequired(response),
      forceFreshBlockhash,
      resourceFallback
    );
  }
  /** Read a `402`'s payment requirements from its header, or its JSON body. */
  async readPaymentRequired(response) {
    let paymentHeader = response.headers.get("payment-required");
    if (!paymentHeader) {
      try {
        const respBody = await response.json();
        if (respBody.accepts || respBody.x402Version) {
          paymentHeader = btoa(JSON.stringify(respBody));
        }
      } catch {
      }
    }
    if (!paymentHeader) {
      throw new PaymentError("402 response but no payment requirements found");
    }
    return parsePaymentRequired(paymentHeader);
  }
  /** Sign the `exact` accept of a 402 as an SPL TransferChecked payment. */
  async signExactPayment(url, paymentRequired, forceFreshBlockhash, resourceFallback = url) {
    const details = extractPaymentDetails(
      { ...paymentRequired, accepts: paymentRequired.accepts.filter((accept) => accept.scheme !== BATCH_SCHEME) },
      SOLANA_NETWORK
    );
    if (!details.network?.startsWith("solana:")) {
      throw new PaymentError(
        `Expected Solana payment network, got: ${details.network}. Use LLMClient for Base payments.`
      );
    }
    const feePayer = details.extra?.feePayer;
    if (!feePayer) throw new PaymentError("Missing feePayer in 402 extra field");
    const fromAddress = await this.getWalletAddress();
    const secretKey = await solanaKeyToBytes(this.privateKey);
    const extensions = withPaymentIdentifier(paymentRequired).extensions;
    const paymentPayload = await createSolanaPaymentPayload(
      secretKey,
      fromAddress,
      details.recipient,
      details.amount,
      feePayer,
      {
        resourceUrl: validateResourceUrl(
          details.resource?.url || resourceFallback,
          this.apiUrl
        ),
        resourceDescription: details.resource?.description || "BlockRun Solana AI API call",
        maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
        extra: details.extra,
        extensions,
        rpcUrl: this.rpcUrl,
        rpcHeaders: this.rpcHeaders,
        forceFreshBlockhash
      }
    );
    return { paymentPayload, costUsd: parseFloat(details.amount) / 1e6 };
  }
  async handlePaymentAndRetry(url, json, paymentRequired, forceFreshBlockhash = false, timeoutMs) {
    let signed;
    try {
      signed = await this.signExactPayment(url, paymentRequired, forceFreshBlockhash, `${this.apiUrl}/v1/chat/completions`);
    } catch (error) {
      throw withDisposition(error, "unpaid");
    }
    const { paymentPayload, costUsd } = signed;
    let retryResponse;
    try {
      retryResponse = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "User-Agent": USER_AGENT,
          "PAYMENT-SIGNATURE": paymentPayload
        },
        body: json
      }, timeoutMs);
    } catch (error) {
      throw withDisposition(error, "paid-or-in-doubt");
    }
    await this.assertPaid(retryResponse);
    this.recordSettlement(costUsd, retryResponse);
    const paid = await this.readPaidJson(retryResponse);
    return withCost(paid, retryResponse, costUsd, "exact");
  }
  async requestWithPaymentRaw(endpoint, body, media = {}) {
    const url = `${this.apiUrl}${endpoint}`;
    const pollBudgetMs = media.pollBudgetMs ?? this.timeout;
    for (let staleRetries = 0; ; ) {
      const response = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
        body: JSON.stringify(body)
      }, media.submitTimeoutMs);
      if (response.status === 402) {
        try {
          return await this.handlePaymentAndRetryRaw(url, body, response, staleRetries > 0, media);
        } catch (error) {
          if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
          await waitForStaleRetry(staleRetries++);
          continue;
        }
      }
      if (!response.ok) {
        let errorBody;
        try {
          errorBody = await response.json();
        } catch {
          errorBody = { error: "Request failed" };
        }
        throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
      }
      if (this.apiAuth && /^\/v1\/(images|videos|audio)\//.test(endpoint)) {
        return this.apiAuth.poll(
          response,
          pollBudgetMs,
          endpoint.startsWith("/v1/videos/") ? VIDEO_POLL_INTERVAL_MS : 2e3
        );
      }
      return response.json();
    }
  }
  async handlePaymentAndRetryRaw(url, body, response, forceFreshBlockhash = false, media = {}) {
    const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
    let retryResponse;
    try {
      retryResponse = await this.fetchWithTimeout(url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "User-Agent": USER_AGENT,
          "PAYMENT-SIGNATURE": paymentPayload
        },
        body: JSON.stringify(body)
      }, media.submitTimeoutMs);
    } catch (error) {
      throw withDisposition(error, "paid-or-in-doubt");
    }
    await this.assertPaid(retryResponse);
    if (retryResponse.status === 202) {
      const kind = pollKindFor(url);
      if (kind !== "video") this.recordSettlement(costUsd, retryResponse);
      const submitted = await retryResponse.json();
      return this.followSolanaJob(submitted, media.pollBudgetMs ?? this.timeout, kind);
    }
    this.recordSettlement(costUsd, retryResponse);
    return retryResponse.json();
  }
  async getWithPaymentRaw(endpoint, params) {
    const query = params ? "?" + new URLSearchParams(params).toString() : "";
    const url = `${this.apiUrl}${endpoint}${query}`;
    for (let staleRetries = 0; ; ) {
      const response = await this.fetchWithTimeout(url, {
        method: "GET",
        headers: { "User-Agent": USER_AGENT }
      });
      if (response.status === 402) {
        try {
          return await this.handleGetPaymentAndRetryRaw(url, endpoint, params, response, staleRetries > 0);
        } catch (error) {
          if (!(error instanceof SafeStaleBlockhashError) || staleRetries >= STALE_BLOCKHASH_RETRY_BACKOFFS_MS.length) throw error;
          await waitForStaleRetry(staleRetries++);
          continue;
        }
      }
      if (!response.ok) {
        let errorBody;
        try {
          errorBody = await response.json();
        } catch {
          errorBody = { error: "Request failed" };
        }
        throw new APIError(`API error: ${response.status}`, response.status, sanitizeErrorResponse(errorBody));
      }
      return response.json();
    }
  }
  async handleGetPaymentAndRetryRaw(url, endpoint, params, response, forceFreshBlockhash = false) {
    const { paymentPayload, costUsd } = await this.signPaymentFrom402(url, response, forceFreshBlockhash);
    const query = params ? "?" + new URLSearchParams(params).toString() : "";
    const retryUrl = `${this.apiUrl}${endpoint}${query}`;
    const retryResponse = await this.fetchWithTimeout(retryUrl, {
      method: "GET",
      headers: {
        "User-Agent": USER_AGENT,
        "PAYMENT-SIGNATURE": paymentPayload
      }
    });
    await this.assertPaid(retryResponse);
    this.recordSettlement(costUsd, retryResponse);
    return retryResponse.json();
  }
  /**
   * Fail a post-payment response, telling a re-signable rejection from a real one.
   *
   * A `402` here is not "pay again": it is the gateway refusing the payment we
   * just signed. Only a rejection the gateway attributes to the VERIFICATION
   * phase is safe to re-sign — anything settled, or ambiguous about which
   * phase it failed in, could already have moved USDC, and re-signing it would
   * pay twice. {@link isSafeStaleBlockhashResponse} is where that judgement
   * lives.
   * @param response - the reply to the paid request.
   * @throws SafeStaleBlockhashError when the caller should re-sign, PaymentError
   *   when it should not, APIError for any other failure. Every one but the
   *   first is marked `"paid-or-in-doubt"`: the payment may have settled.
   */
  async assertPaid(response) {
    if (response.status === 402) {
      if (await isSafeStaleBlockhashResponse(response)) {
        throw withDisposition(new SafeStaleBlockhashError(), "unpaid");
      }
      throw withDisposition(new PaymentError("Payment was rejected. Check your Solana USDC balance."), "paid-or-in-doubt");
    }
    if (!response.ok) {
      let errorBody;
      try {
        errorBody = await response.json();
      } catch {
        errorBody = { error: "Request failed" };
      }
      const unverified = response.status === 503 && errorBody?.code === "PAYMENT_VERIFICATION_UNAVAILABLE";
      throw withDisposition(
        new APIError(`API error after payment: ${response.status}`, response.status, sanitizeErrorResponse(errorBody)),
        unverified ? "unpaid" : "paid-or-in-doubt"
      );
    }
  }
  /**
   * Count one settled x402 payment against the session total.
   *
   * Takes the paid response so no booking site can forget the check: a
   * response whose settlement the gateway skipped ({@link settlementSkipped}:
   * `X-Settlement-Skipped: free-fallback | empty-result | unexecuted | …`, or
   * `X-Payment-Waived`) moved nothing on chain and books nothing.
   * `X-Payment-Settled: false` alone is still booked: the payment is owed.
   */
  recordSettlement(costUsd, response) {
    if (settlementSkipped(response)) return;
    this.sessionCalls += 1;
    this.sessionTotalUsd += costUsd;
  }
  async fetchWithTimeout(url, options, timeoutMs = this.timeout) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await (this.apiAuth ? this.apiAuth.fetch.bind(this.apiAuth) : fetch)(url, { ...options, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }
};
function solanaClient(options = {}) {
  return new SolanaLLMClient({ ...options, apiUrl: SOLANA_API_URL });
}

// src/cache.ts
var fs7 = __toESM(require("fs"), 1);
var path6 = __toESM(require("path"), 1);
var os4 = __toESM(require("os"), 1);
var crypto2 = __toESM(require("crypto"), 1);
var CACHE_DIR = path6.join(os4.homedir(), ".blockrun", "cache");
var DATA_DIR = path6.join(os4.homedir(), ".blockrun", "data");
var COST_LOG_FILE2 = path6.join(os4.homedir(), ".blockrun", "cost_log.jsonl");
var DEFAULT_TTL = {
  "/v1/partner/": 3600 * 1e3,
  "/v1/pm/": 1800 * 1e3,
  "/v1/chat/": 0,
  "/v1/search": 900 * 1e3,
  "/v1/image": 0,
  "/v1/models": 86400 * 1e3
};
function getTtl(endpoint) {
  for (const [pattern, ttl] of Object.entries(DEFAULT_TTL)) {
    if (endpoint.includes(pattern)) return ttl;
  }
  return 3600 * 1e3;
}
function cacheKey(endpoint, body) {
  const keyData = JSON.stringify({ endpoint, body }, Object.keys({ endpoint, body }).sort());
  return crypto2.createHash("sha256").update(keyData).digest("hex").slice(0, 16);
}
function cachePath(key) {
  return path6.join(CACHE_DIR, `${key}.json`);
}
function getCached(key) {
  const filePath = cachePath(key);
  if (!fs7.existsSync(filePath)) return null;
  try {
    const raw = fs7.readFileSync(filePath, "utf-8");
    const entry = JSON.parse(raw);
    const ttl = entry.ttlMs ?? getTtl(entry.endpoint ?? "");
    if (ttl <= 0) return null;
    if (Date.now() - entry.cachedAt > ttl) {
      try {
        fs7.unlinkSync(filePath);
      } catch {
      }
      return null;
    }
    return entry.response;
  } catch {
    return null;
  }
}
function getCachedByRequest(endpoint, body) {
  const ttl = getTtl(endpoint);
  if (ttl <= 0) return null;
  const key = cacheKey(endpoint, body);
  return getCached(key);
}
function setCache(key, data, ttlMs) {
  if (ttlMs <= 0) return;
  try {
    fs7.mkdirSync(CACHE_DIR, { recursive: true });
  } catch {
  }
  const entry = {
    cachedAt: Date.now(),
    response: data,
    ttlMs
  };
  try {
    fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
  } catch {
  }
}
function readableFilename(endpoint, body) {
  const now = /* @__PURE__ */ new Date();
  const ts = now.toISOString().slice(0, 10) + "_" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
  let ep = endpoint.replace(/\/+$/, "").split("/").pop() || "";
  if (endpoint.includes("/v1/chat/")) {
    ep = "chat";
  } else if (endpoint.includes("/v1/search")) {
    ep = "search";
  } else if (endpoint.includes("/v1/image")) {
    ep = "image";
  }
  let label = body.query || body.username || body.handle || body.model || (typeof body.prompt === "string" ? body.prompt.slice(0, 40) : "") || "";
  label = String(label).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40).replace(/^_+|_+$/g, "");
  return label ? `${ep}_${ts}_${label}.json` : `${ep}_${ts}.json`;
}
function saveReadable(endpoint, body, response, costUsd) {
  try {
    fs7.mkdirSync(DATA_DIR, { recursive: true });
  } catch {
  }
  const filename = readableFilename(endpoint, body);
  const entry = {
    saved_at: (/* @__PURE__ */ new Date()).toISOString(),
    endpoint,
    cost_usd: costUsd,
    request: body,
    response
  };
  try {
    fs7.writeFileSync(path6.join(DATA_DIR, filename), JSON.stringify(entry, null, 2));
  } catch {
  }
}
function appendCostLog(endpoint, costUsd) {
  if (costUsd <= 0) return;
  try {
    fs7.mkdirSync(path6.dirname(COST_LOG_FILE2), { recursive: true });
  } catch {
  }
  const entry = {
    ts: Date.now() / 1e3,
    endpoint,
    cost_usd: costUsd
  };
  try {
    fs7.appendFileSync(COST_LOG_FILE2, JSON.stringify(entry) + "\n");
  } catch {
  }
}
function saveToCache(endpoint, body, response, costUsd = 0) {
  const ttl = getTtl(endpoint);
  if (ttl > 0) {
    try {
      fs7.mkdirSync(CACHE_DIR, { recursive: true });
    } catch {
    }
    const key = cacheKey(endpoint, body);
    const entry = {
      cachedAt: Date.now(),
      endpoint,
      body,
      response,
      costUsd
    };
    try {
      fs7.writeFileSync(cachePath(key), JSON.stringify(entry));
    } catch {
    }
  }
  saveReadable(endpoint, body, response, costUsd);
  appendCostLog(endpoint, costUsd);
}
function clearCache() {
  if (!fs7.existsSync(CACHE_DIR)) return 0;
  let count = 0;
  try {
    const files = fs7.readdirSync(CACHE_DIR);
    for (const file of files) {
      if (file.endsWith(".json")) {
        try {
          fs7.unlinkSync(path6.join(CACHE_DIR, file));
          count++;
        } catch {
        }
      }
    }
  } catch {
  }
  return count;
}
function getCostLogSummary() {
  if (!fs7.existsSync(COST_LOG_FILE2)) {
    return { totalUsd: 0, calls: 0, byEndpoint: {} };
  }
  let totalUsd = 0;
  let calls = 0;
  const byEndpoint = {};
  try {
    const content = fs7.readFileSync(COST_LOG_FILE2, "utf-8").trim();
    if (!content) return { totalUsd: 0, calls: 0, byEndpoint: {} };
    for (const line of content.split("\n")) {
      if (!line) continue;
      try {
        const entry = JSON.parse(line);
        const cost = entry.cost_usd ?? 0;
        const ep = entry.endpoint ?? "unknown";
        totalUsd += cost;
        calls += 1;
        byEndpoint[ep] = (byEndpoint[ep] || 0) + cost;
      } catch {
      }
    }
  } catch {
  }
  return { totalUsd, calls, byEndpoint };
}

// src/setup.ts
var import_node_fs = require("fs");
var import_node_os = require("os");
var import_node_path = require("path");
function setupAgentWallet(options) {
  const { address, privateKey, isNew } = getOrCreateWallet();
  if (isNew) {
    const notice = formatWalletMigrationNotice(address);
    if (notice) console.error(notice);
    if (!options?.silent) {
      console.error(
        `
BlockRun Agent Wallet Created!
Address: ${address}
Send USDC on Base to get started.
`
      );
    }
  }
  return new LLMClient({ privateKey });
}
async function setupAgentSolanaWallet(options) {
  const result = await getOrCreateSolanaWallet();
  if (result.isNew) {
    const notice = await formatSolanaWalletMigrationNotice(result.address);
    if (notice) console.error(notice);
    if (!options?.silent) {
      console.error(
        `
BlockRun Solana Agent Wallet Created!
Address: ${result.address}
Send USDC on Solana to get started.
`
      );
    }
  }
  return new SolanaLLMClient({ privateKey: result.privateKey });
}
async function status() {
  const client = setupAgentWallet({ silent: true });
  const address = client.getWalletAddress();
  const balance = await client.getBalance();
  console.log(`Wallet: ${address}`);
  console.log(`Balance: $${balance.toFixed(2)} USDC`);
  return { address, balance };
}
async function setupAgentClient(options = {}) {
  if (resolveApiKeyAuth(options)) return new LLMClient({ apiKey: options.apiKey });
  let chain = options.chain;
  if (!chain) {
    for (const file of ["payment-chain", ".chain"]) {
      try {
        const saved = (0, import_node_fs.readFileSync)((0, import_node_path.join)((0, import_node_os.homedir)(), ".blockrun", file), "utf8").trim();
        if (saved === "solana" || saved === "base") {
          chain = saved;
          break;
        }
      } catch {
      }
    }
  }
  chain ??= !loadSolanaWallet() && loadWallet() ? "base" : "solana";
  return chain === "solana" ? setupAgentSolanaWallet(options) : setupAgentWallet(options);
}

// src/openai-compat.ts
var StreamingResponse = class {
  reader;
  decoder;
  buffer = "";
  model;
  id;
  constructor(response, model) {
    if (!response.body) {
      throw new Error("Response body is null");
    }
    this.reader = response.body.getReader();
    this.decoder = new TextDecoder();
    this.model = model;
    this.id = `chatcmpl-${Date.now()}`;
  }
  async *[Symbol.asyncIterator]() {
    try {
      while (true) {
        const { done, value } = await this.reader.read();
        if (done) break;
        this.buffer += this.decoder.decode(value, { stream: true });
        const lines = this.buffer.split("\n");
        this.buffer = lines.pop() || "";
        for (const line of lines) {
          const trimmed = line.trim();
          if (!trimmed || !trimmed.startsWith("data: ")) continue;
          const data = trimmed.slice(6);
          if (data === "[DONE]") return;
          try {
            const parsed = JSON.parse(data);
            yield this.transformChunk(parsed);
          } catch {
          }
        }
      }
    } finally {
      this.reader.releaseLock();
    }
  }
  transformChunk(data) {
    const choices = data.choices || [];
    return {
      id: data.id || this.id,
      object: "chat.completion.chunk",
      created: data.created || Math.floor(Date.now() / 1e3),
      model: data.model || this.model,
      choices: choices.map((choice, index) => ({
        index: choice.index ?? index,
        delta: {
          role: choice.delta?.role,
          content: choice.delta?.content
        },
        finish_reason: choice.finish_reason || null
      }))
    };
  }
};
var ChatCompletions = class {
  constructor(client, apiUrl, timeout) {
    this.client = client;
    this.apiUrl = apiUrl;
    this.timeout = timeout;
  }
  client;
  apiUrl;
  timeout;
  async create(params) {
    if (params.stream) {
      return this.createStream(params);
    }
    const response = await this.client.chatCompletion(
      params.model,
      params.messages,
      {
        maxTokens: params.max_tokens,
        temperature: params.temperature,
        topP: params.top_p,
        tools: params.tools,
        toolChoice: params.tool_choice,
        responseFormat: params.response_format,
        stop: params.stop
      }
    );
    return this.transformResponse(response);
  }
  async createStream(params) {
    const response = await this.client.chatCompletionStream(
      params.model,
      params.messages,
      {
        maxTokens: params.max_tokens,
        temperature: params.temperature,
        topP: params.top_p,
        tools: params.tools,
        toolChoice: params.tool_choice,
        responseFormat: params.response_format,
        stop: params.stop
      }
    );
    return new StreamingResponse(response, params.model);
  }
  transformResponse(response) {
    return {
      id: response.id || `chatcmpl-${Date.now()}`,
      object: "chat.completion",
      created: response.created || Math.floor(Date.now() / 1e3),
      model: response.model,
      choices: response.choices.map((choice, index) => ({
        index: choice.index ?? index,
        message: {
          role: "assistant",
          content: choice.message.content,
          tool_calls: choice.message.tool_calls
        },
        finish_reason: choice.finish_reason || "stop"
      })),
      usage: response.usage,
      routing: response.routing
    };
  }
};
var Chat = class {
  completions;
  constructor(client, apiUrl, timeout) {
    this.completions = new ChatCompletions(client, apiUrl, timeout);
  }
};
var OpenAI = class {
  chat;
  get authMode() {
    return this.client.authMode;
  }
  client;
  constructor(options = {}) {
    const aliases = [options.walletKey, options.privateKey].filter((k) => k !== void 0);
    const privateKey = aliases.find((k) => k !== "") ?? aliases[0];
    const apiUrl = resolveApiKeyAuth({ apiKey: options.apiKey, privateKey, apiUrl: options.baseURL })?.apiUrl ?? options.baseURL ?? "https://blockrun.ai/api";
    const timeout = options.timeout ?? DEFAULT_TIMEOUT;
    this.client = new LLMClient({
      privateKey,
      apiKey: options.apiKey,
      apiUrl,
      timeout
    });
    this.chat = new Chat(this.client, apiUrl, timeout);
  }
  /**
   * Get the wallet address being used for payments.
   */
  getWalletAddress() {
    return this.client.getWalletAddress();
  }
};

// src/anthropic-compat.ts
var import_accounts17 = require("viem/accounts");
var AnthropicClient = class {
  _client = null;
  _clientPromise = null;
  apiAuth;
  get authMode() {
    return this.apiAuth ? "api-key" : "wallet";
  }
  walletKey;
  get _privateKey() {
    return requireWallet(this.walletKey);
  }
  walletAccount;
  get _account() {
    return requireWallet(this.walletAccount);
  }
  _apiUrl;
  _timeout;
  constructor(options = {}) {
    this.apiAuth = resolveApiKeyAuth(options);
    if (!this.apiAuth) {
      const key = options.privateKey ?? getOrCreateWallet().privateKey;
      validatePrivateKey(key);
      this.walletKey = key;
      this.walletAccount = (0, import_accounts17.privateKeyToAccount)(this.walletKey);
    }
    const apiUrl = this.apiAuth?.apiUrl ?? options.apiUrl ?? "https://blockrun.ai/api";
    validateApiUrl(apiUrl);
    this._apiUrl = apiUrl.replace(/\/$/, "");
    this._timeout = options.timeout ?? DEFAULT_TIMEOUT;
  }
  async _getClient() {
    if (this._client) return this._client;
    if (this._clientPromise) return this._clientPromise;
    this._clientPromise = (async () => {
      const { default: Anthropic } = await import("@anthropic-ai/sdk");
      this._client = new Anthropic({
        baseURL: this._apiUrl,
        apiKey: "blockrun",
        fetch: this._x402Fetch.bind(this),
        // Never let the Anthropic SDK retry. It retries 408/409/429/5xx and
        // connection errors by calling this fetch again, which in wallet mode
        // runs the whole x402 flow and signs a NEW payment each time (three
        // payments for one call at the default maxRetries of 2); in account
        // mode the POST itself may already be billed.
        maxRetries: 0
      });
      return this._client;
    })();
    return this._clientPromise;
  }
  async _x402Fetch(input, init) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this._timeout);
    const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : void 0);
    const onAbort = () => controller.abort(callerSignal?.reason);
    callerSignal?.addEventListener("abort", onAbort, { once: true });
    if (callerSignal?.aborted) onAbort();
    try {
      const mergedInit = { ...init, signal: controller.signal };
      if (this.apiAuth) return await this.apiAuth.fetch(input, mergedInit, false);
      let response = await globalThis.fetch(input, mergedInit);
      if (response.status === 402) {
        let paymentHeader = response.headers.get("payment-required");
        if (!paymentHeader) {
          try {
            const respBody = await response.json();
            if (respBody.x402 || respBody.accepts) {
              paymentHeader = btoa(JSON.stringify(respBody));
            }
          } catch {
          }
        }
        if (!paymentHeader) {
          throw new Error("402 response but no payment requirements found");
        }
        const paymentRequired = parsePaymentRequired(paymentHeader);
        const details = extractPaymentDetails(paymentRequired);
        const extensions = paymentRequired.extensions;
        const paymentPayload = await createPaymentPayload(
          this._privateKey,
          this._account.address,
          details.recipient,
          details.amount,
          details.network || "eip155:8453",
          {
            resourceUrl: validateResourceUrl(
              details.resource?.url || `${this._apiUrl}/v1/messages`,
              this._apiUrl
            ),
            resourceDescription: details.resource?.description || "BlockRun AI API call",
            maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
            extra: details.extra,
            asset: details.asset,
            extensions
          }
        );
        const newHeaders = new Headers(init?.headers);
        newHeaders.set("PAYMENT-SIGNATURE", paymentPayload);
        response = await globalThis.fetch(input, {
          ...init,
          headers: newHeaders,
          signal: controller.signal
        });
        if (response.status === 402) {
          throw new Error(
            "Payment was rejected. Check your wallet balance."
          );
        }
      }
      return response;
    } finally {
      callerSignal?.removeEventListener("abort", onAbort);
      clearTimeout(timeoutId);
    }
  }
  get messages() {
    const handler = {
      get: (_target, prop) => {
        return async (...args) => {
          const client = await this._getClient();
          const messages = client.messages;
          return messages[prop](...args);
        };
      }
    };
    return new Proxy({}, handler);
  }
  getWalletAddress() {
    return this._account.address;
  }
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  APIError,
  API_KEY_URL,
  ARC_CHAIN_ID,
  AnthropicClient,
  BASE_CHAIN_ID,
  BLOCKRUN_SOL_OPERATOR,
  BatchChannelLockedError,
  BatchCloseDeferredError,
  BatchPaymentUnresolvedError,
  BlockrunClient,
  BlockrunError,
  DEFAULT_BATCH_IN_DOUBT,
  DEFAULT_BATCH_LOCK_WAIT_MS,
  DEFAULT_BATCH_RATE_LIMIT,
  EVM_NETWORKS,
  ImageClient,
  KNOWN_PROVIDERS,
  LLMClient,
  MusicClient,
  NETWORK_ALIASES,
  OpenAI,
  PHONE_PRICES,
  PORTAL_URL,
  PORTRAIT_ENROLLMENT_PRICE_USD,
  PaymentError,
  PhoneClient,
  PortraitClient,
  PriceClient,
  RPC_PRICE_USD,
  RetiredEndpointError,
  RpcClient,
  SOLANA_NETWORK,
  SOLANA_WALLET_FILE_PATH,
  SUPPORTED_NETWORKS,
  SearchClient,
  SolanaBatchPayer,
  SolanaLLMClient,
  SpeechClient,
  SurfClient,
  USDC_ARC,
  USDC_BASE,
  USDC_BASE_CONTRACT,
  USDC_SOLANA,
  VideoClient,
  VoiceClient,
  WALLET_DIR_PATH,
  WALLET_FILE_PATH,
  clearCache,
  createPaymentPayload,
  createSolanaPaymentPayload,
  createSolanaWallet,
  createWallet,
  evmNetwork,
  extractPaymentDetails,
  formatFundingMessageCompact,
  formatNeedsFundingMessage,
  formatSolanaWalletMigrationNotice,
  formatWalletCreatedMessage,
  formatWalletMigrationNotice,
  getCached,
  getCachedByRequest,
  getCostLogSummary,
  getCostSummary,
  getEip681Uri,
  getOrCreateSolanaWallet,
  getOrCreateWallet,
  getPaymentLinks,
  getWalletAddress,
  importSolanaWallet,
  importWallet,
  listDiscoveredSolanaWallets,
  listDiscoveredWallets,
  loadSolanaWallet,
  loadWallet,
  logCost,
  parsePaymentRequired,
  readSettlement,
  retryDisposition,
  saveSolanaWallet,
  saveToCache,
  saveWallet,
  scanSolanaWallets,
  scanWallets,
  setCache,
  setupAgentClient,
  setupAgentSolanaWallet,
  setupAgentWallet,
  solanaClient,
  solanaKeyToBytes,
  solanaPublicKey,
  status,
  toWireSearchParameters,
  toWireTools,
  validateMaxTokens,
  validateModel,
  validateTemperature,
  validateTopP
});