pullfrog 0.1.100 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64099,7 +64099,7 @@ var require_receiver2 = __commonJS({
64099
64099
  const buffer = this.consume(2);
64100
64100
  const fin = (buffer[0] & 128) !== 0;
64101
64101
  const opcode = buffer[0] & 15;
64102
- const masked = (buffer[1] & 128) === 128;
64102
+ const masked2 = (buffer[1] & 128) === 128;
64103
64103
  const fragmented = !fin && opcode !== opcodes.CONTINUATION;
64104
64104
  const payloadLength = buffer[1] & 127;
64105
64105
  const rsv1 = buffer[0] & 64;
@@ -64109,7 +64109,7 @@ var require_receiver2 = __commonJS({
64109
64109
  failWebsocketConnection(this.#handler, 1002, "Invalid opcode received");
64110
64110
  return callback();
64111
64111
  }
64112
- if (masked) {
64112
+ if (masked2) {
64113
64113
  failWebsocketConnection(this.#handler, 1002, "Frame cannot be masked");
64114
64114
  return callback();
64115
64115
  }
@@ -64157,7 +64157,7 @@ var require_receiver2 = __commonJS({
64157
64157
  this.#info.compressed = rsv1 !== 0;
64158
64158
  }
64159
64159
  this.#info.opcode = opcode;
64160
- this.#info.masked = masked;
64160
+ this.#info.masked = masked2;
64161
64161
  this.#info.fin = fin;
64162
64162
  this.#info.fragmented = fragmented;
64163
64163
  } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
@@ -104868,6 +104868,13 @@ if (!defaultProxyAlias?.openRouterResolve) {
104868
104868
  }
104869
104869
  var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
104870
104870
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
104871
+ var trialProxyAlias = resolveDisplayAlias("openai/gpt-luna");
104872
+ if (!trialProxyAlias?.openRouterResolve) {
104873
+ throw new Error("TRIAL_PROXY_MODEL: openai/gpt-luna has no openRouterResolve");
104874
+ }
104875
+ var TRIAL_PROXY_MODEL = trialProxyAlias.openRouterResolve;
104876
+ var TRIAL_PROXY_DISPLAY_NAME = trialProxyAlias.displayName;
104877
+ var TRIAL_FUNDED_USD_LIMIT = 10;
104871
104878
  for (const [name, ladder] of Object.entries({ ...PROVIDER_LADDERS, router: ROUTER_LADDER })) {
104872
104879
  const terminal = /* @__PURE__ */ new Set();
104873
104880
  for (const slug2 of Object.values(ladder)) {
@@ -105575,6 +105582,8 @@ var PROVIDER_ERROR_PATTERNS = [
105575
105582
  },
105576
105583
  // OpenAI's `You have no credits remaining` (#1474).
105577
105584
  { regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
105585
+ // Codex on a ChatGPT workspace: `Your workspace is out of credits. Add credits to continue.`
105586
+ { regex: /workspace is out of credits/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
105578
105587
  { regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
105579
105588
  // "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
105580
105589
  // condition in two house styles — a configured ceiling was reached.
@@ -105604,6 +105613,9 @@ var PROVIDER_ERROR_PATTERNS = [
105604
105613
  // succeeds" while the same log held nine of these. Measured on a live account
105605
105614
  // whose ChatGPT subscription was capped.
105606
105615
  { regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
105616
+ // the Codex CLI's wording for the same capped plan: `You’ve hit your usage limit. … try
105617
+ // again at 7:58 AM.` Claude's `hit your session limit` names its window and stays out.
105618
+ { regex: /\bhit your usage limit\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
105607
105619
  // the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
105608
105620
  // run those mean the PULLFROG wallet is empty and `renderRunError` returns a
105609
105621
  // `BillingError` before ever reaching this list; on a BYOK run the very same
@@ -105708,7 +105720,8 @@ function isProviderMissingCredential(text) {
105708
105720
  return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
105709
105721
  }
105710
105722
  function isTransientUpstreamError(text) {
105711
- return /API Error:\s*5\d\d/i.test(text) || /\bOverloaded\b/.test(text) || /"error_type"\s*:\s*"(?:provider_unavailable|timeout)"/i.test(text) || /Streaming response failed:\s*\[5\d\d\]/i.test(text);
105723
+ return /API Error:\s*5\d\d/i.test(text) || /\bOverloaded\b/.test(text) || /"error_type"\s*:\s*"(?:provider_unavailable|timeout)"/i.test(text) || /Streaming response failed:\s*\[5\d\d\]/i.test(text) || // the Codex CLI's `Selected model is at capacity. Please try a different model.`
105724
+ /\bmodel is at capacity\b/i.test(text);
105712
105725
  }
105713
105726
  function extractProviderId(text) {
105714
105727
  const match3 = text.match(/\bproviderID=([a-z0-9_-]+)/i);
@@ -105769,7 +105782,7 @@ var import_semver = __toESM(require_semver2(), 1);
105769
105782
  // package.json
105770
105783
  var package_default = {
105771
105784
  name: "pullfrog",
105772
- version: "0.1.100",
105785
+ version: "0.1.101",
105773
105786
  cliContractVersion: "1",
105774
105787
  type: "module",
105775
105788
  bin: {
@@ -110268,9 +110281,20 @@ var SUBSCRIPTION_DATA_DIR_STATE = "subscription_data_dir";
110268
110281
 
110269
110282
  // utils/secretCommands.ts
110270
110283
  var core3 = __toESM(require_core(), 1);
110284
+ var masked = /* @__PURE__ */ new Set();
110271
110285
  function maskSecret(value2) {
110286
+ masked.add(value2);
110272
110287
  if (isGitHubActions) core3.setSecret(value2);
110273
110288
  }
110289
+ function redactSecrets(text) {
110290
+ let redacted = text;
110291
+ for (const value2 of masked) {
110292
+ for (const part of [value2, ...value2.split("\n").map((line) => line.trim())]) {
110293
+ if (part.length >= 8) redacted = redacted.replaceAll(part, "***");
110294
+ }
110295
+ }
110296
+ return redacted;
110297
+ }
110274
110298
  function saveSecretState(name, value2) {
110275
110299
  if (isGitHubActions) core3.saveState(name, value2);
110276
110300
  }
@@ -189092,7 +189116,7 @@ function formatModelLabel(params) {
189092
189116
  }
189093
189117
  const base = alias?.isFree ? `\`${modelName}\` (free)` : `\`${modelName}\``;
189094
189118
  if (params.clamped?.reason === "trial") {
189095
- return `${base} (model usage covered by Pullfrog)`;
189119
+ return `${base} (free trial credits)`;
189096
189120
  }
189097
189121
  if (params.clamped) {
189098
189122
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
@@ -189103,13 +189127,14 @@ function formatModelLabel(params) {
189103
189127
  }
189104
189128
  return base;
189105
189129
  }
189106
- function buildTrialDisclosure(owner) {
189107
- const consoleUrl = owner ? `https://pullfrog.com/console/${owner}` : "https://pullfrog.com/console";
189130
+ function buildTrialDisclosure(params) {
189131
+ const consoleUrl = params.owner ? `https://pullfrog.com/console/${params.owner}#billing` : "https://pullfrog.com/console";
189132
+ const left = params.creditsLeftUsd === void 0 ? "" : ` ($${Math.max(0, params.creditsLeftUsd).toFixed(2)} of $${TRIAL_FUNDED_USD_LIMIT} left)`;
189108
189133
  return [
189109
189134
  "---",
189110
189135
  "",
189111
189136
  "> [!IMPORTANT]",
189112
- `> **Pullfrog covered this run's model usage.** \`DeepSeek Flash\` is fast and cheap \u2014 expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. [Connect a model-provider subscription or API key \u2192](${consoleUrl})`
189137
+ `> **This run used your free trial credits**${left} on \`${TRIAL_PROXY_DISPLAY_NAME}\`, a fast, lightweight model. [Connect a provider \u2192](${consoleUrl}) to run the model you choose.`
189113
189138
  ].join("\n");
189114
189139
  }
189115
189140
  function buildPullfrogFooter(params) {
@@ -189132,13 +189157,13 @@ function buildPullfrogFooter(params) {
189132
189157
  `Using ${formatModelLabel({
189133
189158
  model: params.model,
189134
189159
  clamped: params.clamped,
189135
- unselectedProxyDefault: params.unselectedProxyDefault,
189160
+ unselectedProxyDefault: params.unselectedProxyDefault && !params.failed,
189136
189161
  oss: params.oss
189137
189162
  })}`
189138
189163
  );
189139
189164
  }
189140
- const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
189141
- const disclosure = params.clamped?.reason === "trial" ? `${buildTrialDisclosure(params.owner)}
189165
+ const allParts = params.failed ? parts : [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
189166
+ const disclosure = params.clamped?.reason === "trial" && !params.failed ? `${buildTrialDisclosure({ owner: params.owner, creditsLeftUsd: params.clamped.creditsLeftUsd })}
189142
189167
 
189143
189168
  ` : "";
189144
189169
  return `
@@ -189316,7 +189341,8 @@ async function fetchRunContext(params) {
189316
189341
  credentialAccess: data.credentialAccess,
189317
189342
  secretsUnavailable: data.secretsUnavailable,
189318
189343
  routerUnfunded: data.routerUnfunded,
189319
- trialFallback: data.trialFallback
189344
+ trialFallback: data.trialFallback,
189345
+ trialCreditsLeftUsd: data.trialCreditsLeftUsd
189320
189346
  };
189321
189347
  } catch {
189322
189348
  clearTimeout(timeoutId);
@@ -189430,6 +189456,9 @@ function aggregateUsage(entries) {
189430
189456
  return out;
189431
189457
  }
189432
189458
 
189459
+ // utils/leapingComment.ts
189460
+ var TODO_SPINNER_URL = "https://uploads.pullfrog.com/Progress%20Indicator.gif";
189461
+
189433
189462
  // mcp/comment.ts
189434
189463
  function isNotFoundError(error49) {
189435
189464
  return error49 instanceof Error && error49.message.includes("Not Found");
@@ -191541,7 +191570,7 @@ function mirrorRolePermissions(role) {
191541
191570
  case "maintain":
191542
191571
  case "write":
191543
191572
  case "triage":
191544
- return { contents: "read", issues: "write", pull_requests: "write" };
191573
+ return { checks: "read", contents: "read", issues: "write", pull_requests: "write" };
191545
191574
  case "read":
191546
191575
  case "none":
191547
191576
  case void 0:
@@ -194210,6 +194239,40 @@ function subagentDeniedToolNames(ctx, outputSchema) {
194210
194239
  return names;
194211
194240
  }
194212
194241
 
194242
+ // utils/failureBody.ts
194243
+ function renderFailure(params) {
194244
+ const blocks = [
194245
+ params.detail ? `**${params.headline}** ${params.detail}` : `**${params.headline}**`
194246
+ ];
194247
+ if (params.list) blocks.push(params.list.map((item) => `- ${item}`).join("\n"));
194248
+ if (params.error) blocks.push(fenced(params.error));
194249
+ if (params.details) {
194250
+ blocks.push(
194251
+ `<details><summary>${params.details.summary}</summary>
194252
+
194253
+ ${fenced(params.details.text)}
194254
+
194255
+ </details>`
194256
+ );
194257
+ }
194258
+ if (params.links?.length) {
194259
+ blocks.push(params.links.map((link) => `[${link.label} \u2192](${link.url})`).join(" \xB7 "));
194260
+ }
194261
+ return blocks.join("\n\n");
194262
+ }
194263
+ function fenced(text) {
194264
+ let longest = 0;
194265
+ for (const match3 of text.matchAll(/`+/g)) longest = Math.max(longest, match3[0].length);
194266
+ const fence = "`".repeat(Math.max(3, longest + 1));
194267
+ return `${fence}
194268
+ ${text}
194269
+ ${fence}`;
194270
+ }
194271
+ var DISCORD_LINK = {
194272
+ label: "Ask in Discord",
194273
+ url: "https://discord.gg/8y96raFg8e"
194274
+ };
194275
+
194213
194276
  // utils/providerDashboards.ts
194214
194277
  var PROVIDER_DASHBOARDS = {
194215
194278
  anthropic: {
@@ -194320,134 +194383,145 @@ var PROVIDER_DASHBOARDS = {
194320
194383
  };
194321
194384
 
194322
194385
  // utils/apiKeys.ts
194323
- var MISSING_KEY_MARKER = "no API key found";
194324
- var BYOK_SLUG_MARKER = "env var is required when the model is set to";
194325
- var BYOK_CONFIG_MARKER = "selected but required configuration is missing:";
194386
+ var MISSING_KEY_MARKER = "No API key found";
194387
+ var BYOK_SLUG_MARKER = "must be set when the model is";
194388
+ var BYOK_CONFIG_MARKER = "is missing required settings:";
194326
194389
  var BYOK_SETUP_PATTERN = new RegExp(
194327
- `^(?:[A-Z][A-Z0-9_]+ ${BYOK_SLUG_MARKER}|[A-Z][\\w-]*(?: [\\w-]+){0,3} ${BYOK_CONFIG_MARKER})`
194390
+ `^(?:\`[A-Z][A-Z0-9_]+\` ${BYOK_SLUG_MARKER}|[A-Z][\\w-]*(?: [\\w-]+){0,3} ${BYOK_CONFIG_MARKER})`
194328
194391
  );
194329
194392
  function isByokSetupError(text) {
194330
194393
  return BYOK_SETUP_PATTERN.test(text);
194331
194394
  }
194332
- var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
194333
- var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
194395
+ var SECRETS_UNAVAILABLE_MARKER = "couldn't load your stored secrets";
194396
+ var ROUTER_UNFUNDED_MARKER = "Your Pullfrog Router balance is empty";
194397
+ var TRIAL_SPENT_MARKER = "reached the limit of your free trial credits";
194398
+ function buildTrialSpentError(params) {
194399
+ return renderFailure({
194400
+ headline: `You've ${TRIAL_SPENT_MARKER}.`,
194401
+ detail: `Your $${TRIAL_FUNDED_USD_LIMIT} of trial usage credits have been used up, so you'll need to connect a provider to continue using Pullfrog.`,
194402
+ links: [providerLink(params.owner)]
194403
+ });
194404
+ }
194405
+ function repoConsoleUrl(repo) {
194406
+ return `${getApiUrl()}/console/${repo.owner}/${repo.name}`;
194407
+ }
194408
+ function pickModelLink(repo) {
194409
+ return { label: "Pick another model", url: repoConsoleUrl(repo) };
194410
+ }
194411
+ function providerLink(owner) {
194412
+ return { label: "Connect a provider", url: `${getApiUrl()}/console/${owner}#billing` };
194413
+ }
194334
194414
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
194335
194415
  var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
194336
194416
  var NoUsableCredentialError = class extends Error {
194337
194417
  };
194338
194418
  function throwKeyError(params) {
194339
- if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError(params));
194419
+ if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError());
194340
194420
  if (params.routerUnfunded) throw new NoUsableCredentialError(buildRouterUnfundedError(params));
194341
194421
  throw new NoUsableCredentialError(buildMissingApiKeyError(params));
194342
194422
  }
194343
194423
  function buildRouterUnfundedError(params) {
194344
- const billingUrl = `${getApiUrl()}/console/${params.owner}#billing`;
194345
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194346
- const modelClause = params.model ? ` \`${params.model}\` never ran.` : " the agent never ran.";
194347
- return [
194348
- `**${ROUTER_UNFUNDED_MARKER}**, and this repo has no provider key to fall back on, so${modelClause}`,
194349
- "",
194350
- "**To fix, any one of:** add a payment method or top up your Router balance \xB7 add a provider API key (GitHub Actions secret or Pullfrog secret) \xB7 switch this repo to a free model.",
194351
- "",
194352
- `[Top up Router \u2192](${billingUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194353
- ].join("\n");
194424
+ return renderFailure({
194425
+ headline: `${ROUTER_UNFUNDED_MARKER}, and this repo has no API key to fall back on.`,
194426
+ detail: "Add Router credit, or connect your own provider key or subscription.",
194427
+ links: [
194428
+ { label: "Billing and providers", url: `${getApiUrl()}/console/${params.owner}#billing` }
194429
+ ]
194430
+ });
194354
194431
  }
194355
- function buildSecretsUnavailableError(params) {
194356
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194357
- const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
194358
- return [
194359
- `**Pullfrog ${SECRETS_UNAVAILABLE_MARKER}${modelClause} on this run.** The key is still stored \u2014 the runner just couldn't fetch it, so this is a transient failure on our side, not a missing key.`,
194360
- "",
194361
- "**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
194362
- "",
194363
- `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194364
- ].join("\n");
194432
+ function buildSecretsUnavailableError() {
194433
+ return renderFailure({
194434
+ headline: `Pullfrog ${SECRETS_UNAVAILABLE_MARKER} for this run.`,
194435
+ detail: "They're still saved; this is a temporary failure on our side. Re-trigger to try again, and tell us in Discord if it keeps happening.",
194436
+ links: [DISCORD_LINK]
194437
+ });
194365
194438
  }
194366
194439
  function buildMissingApiKeyError(params) {
194367
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194368
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194369
194440
  const envVars = params.model?.includes("/") ? getModelEnvVars(params.model) : [];
194370
194441
  const [primary, ...alternates] = envVars;
194371
194442
  const envVarList = primary ? `\`${primary}\`${alternates.length > 0 ? ` (or ${alternates.map((v) => `\`${v}\``).join(" / ")})` : ""}` : void 0;
194372
- const lead = envVarList ? `**${MISSING_KEY_MARKER}** \u2014 this run used \`${params.model}\`, which needs ${envVarList}, but the runner has no key for it.` : `**${MISSING_KEY_MARKER}** \u2014 Pullfrog needs at least one LLM provider API key (e.g. \`ANTHROPIC_API_KEY\`, \`OPENAI_API_KEY\`, \`GEMINI_API_KEY\`) configured as a GitHub Actions secret.`;
194443
+ const alias = params.model ? resolveDisplayAlias(params.model) ?? modelAliases.find((a) => !a.fallback && a.resolve === params.model) : void 0;
194444
+ const modelName = params.model ? alias?.displayName ?? `\`${params.model}\`` : void 0;
194373
194445
  const unhosted = params.model?.includes("/") ? getModelManagedCredentials(params.model).find((name) => unhostedSubscriptions.has(name)) : void 0;
194374
- const fix = unhosted ? `**To fix:** your ${SUBSCRIPTION_CREDENTIALS[unhosted]?.label} is stored, but this runner has no passwordless \`sudo\`, which Pullfrog needs to keep it in \`${PULLFROG_DATA_DIR}\`, out of the agent's reach. Allow passwordless \`sudo\` for the runner user, run on a GitHub-hosted runner, or add an API key as a GitHub Actions secret or a Pullfrog secret.` : "**To fix:** add the key as a GitHub Actions secret (referenced from your workflow's `env:` block) or as a Pullfrog secret in the console \u2014 or switch this repo to a different model (free models need no key).";
194446
+ const detail = unhosted ? `Your ${SUBSCRIPTION_CREDENTIALS[unhosted]?.label} is stored, but this runner has no passwordless \`sudo\`, which Pullfrog needs to keep it in \`${PULLFROG_DATA_DIR}\`, out of the agent's reach. Allow passwordless \`sudo\` for the runner user, run on a GitHub-hosted runner, or add an API key.` : envVarList ? `It needs ${envVarList}. Connect it in Pullfrog or add it as a GitHub Actions secret, or pick a model you already have a key for.` : "Pullfrog needs a key or subscription for at least one model provider. Connect one in Pullfrog, or add a key as a GitHub Actions secret.";
194447
+ return renderFailure({
194448
+ headline: envVarList && modelName ? `${MISSING_KEY_MARKER} for ${modelName}.` : `${MISSING_KEY_MARKER}.`,
194449
+ detail,
194450
+ links: [providerLink(params.owner), pickModelLink(params)]
194451
+ });
194452
+ }
194453
+ function buildSetupError(params) {
194454
+ const missing = params.missing.map((name) => `\`${name}\``).join(", ");
194375
194455
  return [
194376
- lead,
194377
- "",
194378
- fix,
194379
- "",
194380
- `[Open repo secrets \u2192](${githubSecretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194381
- ].join("\n");
194456
+ `${params.provider} ${BYOK_CONFIG_MARKER} ${missing}. ${params.where}`,
194457
+ `\`\`\`yaml
194458
+ ${params.env.join("\n")}
194459
+ \`\`\``,
194460
+ `${params.note ? `${params.note} ` : ""}[${params.provider} setup guide](${params.guide})`
194461
+ ].join("\n\n");
194462
+ }
194463
+ function secretsUrl(params) {
194464
+ return `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194382
194465
  }
194383
194466
  function buildBedrockSetupError(params) {
194384
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194385
- return `Bedrock model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
194386
-
194387
- add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
194388
-
194389
- AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
194390
- AWS_REGION: \${{ secrets.AWS_REGION }}
194391
- ${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
194392
-
194393
- \`AWS_BEARER_TOKEN_BEDROCK\` may be substituted with \`AWS_ACCESS_KEY_ID\` + \`AWS_SECRET_ACCESS_KEY\` (and optional \`AWS_SESSION_TOKEN\`) if you prefer access keys.
194394
-
194395
- for full setup instructions, see https://docs.pullfrog.com/bedrock`;
194467
+ return buildSetupError({
194468
+ provider: "Amazon Bedrock",
194469
+ missing: params.missing,
194470
+ where: `Add them to [this repo's Actions secrets](${secretsUrl(params)}) and pass them in the workflow's \`env:\` block:`,
194471
+ env: [
194472
+ `AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}`,
194473
+ `AWS_REGION: \${{ secrets.AWS_REGION }}`,
194474
+ `${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}`
194475
+ ],
194476
+ note: "`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (plus `AWS_SESSION_TOKEN` if you use one) can replace the bearer token.",
194477
+ guide: "https://docs.pullfrog.com/bedrock"
194478
+ });
194396
194479
  }
194397
194480
  function buildVertexSetupError(params) {
194398
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194399
- return `Google Vertex AI model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
194400
-
194401
- add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
194402
-
194403
- ${VERTEX_SERVICE_ACCOUNT_JSON_ENV}: \${{ secrets.${VERTEX_SERVICE_ACCOUNT_JSON_ENV} }}
194404
- ${GOOGLE_CLOUD_PROJECT_ENV}: my-project
194405
- ${VERTEX_LOCATION_ENV}: global
194406
- ${VERTEX_MODEL_ID_ENV}: <vertex-model-id>
194407
-
194408
- for full setup instructions, see https://docs.pullfrog.com/vertex`;
194481
+ return buildSetupError({
194482
+ provider: "Google Vertex AI",
194483
+ missing: params.missing,
194484
+ where: `Add the service account to [this repo's Actions secrets](${secretsUrl(params)}) and set the rest in the workflow's \`env:\` block:`,
194485
+ env: [
194486
+ `${VERTEX_SERVICE_ACCOUNT_JSON_ENV}: \${{ secrets.${VERTEX_SERVICE_ACCOUNT_JSON_ENV} }}`,
194487
+ `${GOOGLE_CLOUD_PROJECT_ENV}: my-project`,
194488
+ `${VERTEX_LOCATION_ENV}: global`,
194489
+ `${VERTEX_MODEL_ID_ENV}: <vertex-model-id>`
194490
+ ],
194491
+ guide: "https://docs.pullfrog.com/vertex"
194492
+ });
194409
194493
  }
194410
194494
  function buildOpenAICompatibleSetupError(params) {
194411
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194412
- return `OpenAI-compatible model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
194413
-
194414
- only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
194415
-
194416
- ${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
194417
- ${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
194418
- ${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
194419
- ${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
194420
- ${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
194421
-
194422
- set the last two to the real limits of the model your endpoint serves. Pullfrog can't
194423
- discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
194424
- capped at 32000 tokens (rejected outright by models with a smaller cap) and
194425
- auto-compaction is disabled, so long runs grow until your endpoint refuses them.
194426
-
194427
- for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
194495
+ return buildSetupError({
194496
+ provider: "OpenAI-compatible endpoint",
194497
+ missing: params.missing,
194498
+ where: `Set them on the OpenAI-compatible provider in [Pullfrog's provider settings](${getApiUrl()}/console/${params.owner}#billing), or in the workflow's \`env:\` block with the key as an [Actions secret](${secretsUrl(params)}):`,
194499
+ env: [
194500
+ `${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1`,
194501
+ `${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}`,
194502
+ `${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>`,
194503
+ `${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"`,
194504
+ `${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"`
194505
+ ],
194506
+ note: "Use the real context and output limits of the model your endpoint serves: Pullfrog can't discover them, and without them long runs grow until the endpoint refuses them.",
194507
+ guide: "https://docs.pullfrog.com/openai-compatible"
194508
+ });
194428
194509
  }
194429
194510
  function buildAzureSetupError(params) {
194430
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194431
- return `Azure OpenAI ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
194432
-
194433
- only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. the rest is plain workflow \`env:\`:
194434
-
194435
- ${AZURE_RESOURCE_NAME_ENV}: <name>
194436
- ${AZURE_API_KEY_ENV}: \${{ secrets.${AZURE_API_KEY_ENV} }}
194437
- ${AZURE_DEPLOYMENT_ENV}: <deployment-name>
194438
- ${AZURE_CONTEXT_ENV}: "400000"
194439
- ${AZURE_MAX_OUTPUT_ENV}: "128000"
194440
-
194441
- ${AZURE_RESOURCE_NAME_ENV} is the \`<name>\` in your endpoint https://<name>.openai.azure.com.
194442
- ${AZURE_DEPLOYMENT_ENV} is the name of the deployment, which is not necessarily the name of
194443
- the model it serves \u2014 Azure routes on the deployment name.
194444
-
194445
- set the last two to the real limits of whichever model your deployment serves. Pullfrog can't
194446
- discover them \u2014 a deployment name carries no catalog metadata \u2014 and without them completions
194447
- are capped at 32000 tokens (rejected outright by models with a smaller cap) and auto-compaction
194448
- is disabled, so long runs grow until Azure refuses them.
194449
-
194450
- for full setup instructions, see https://docs.pullfrog.com/azure`;
194511
+ return buildSetupError({
194512
+ provider: "Azure OpenAI",
194513
+ missing: params.missing,
194514
+ where: `Set them on the Azure OpenAI provider in [Pullfrog's provider settings](${getApiUrl()}/console/${params.owner}#billing), or in the workflow's \`env:\` block with the key as an [Actions secret](${secretsUrl(params)}):`,
194515
+ env: [
194516
+ `${AZURE_RESOURCE_NAME_ENV}: <name>`,
194517
+ `${AZURE_API_KEY_ENV}: \${{ secrets.${AZURE_API_KEY_ENV} }}`,
194518
+ `${AZURE_DEPLOYMENT_ENV}: <deployment-name>`,
194519
+ `${AZURE_CONTEXT_ENV}: "400000"`,
194520
+ `${AZURE_MAX_OUTPUT_ENV}: "128000"`
194521
+ ],
194522
+ note: `\`${AZURE_RESOURCE_NAME_ENV}\` is the \`<name>\` in \`https://<name>.openai.azure.com\`, and \`${AZURE_DEPLOYMENT_ENV}\` is the deployment's name, not the model it serves. Use that model's real context and output limits: Pullfrog can't discover them, and without them long runs grow until Azure refuses them.`,
194523
+ guide: "https://docs.pullfrog.com/azure"
194524
+ });
194451
194525
  }
194452
194526
  function hasEnvVar2(name) {
194453
194527
  const value2 = process.env[name];
@@ -194607,7 +194681,7 @@ function isMalformedKeyError(text) {
194607
194681
  return /Header '\d+' has invalid value/i.test(text);
194608
194682
  }
194609
194683
  function isClaudeSubscriptionDisabledError(text) {
194610
- return /disabled Claude subscription access/i.test(text);
194684
+ return /disabled Claude subscription access/i.test(text) || /OAuth authentication is currently not allowed for this organization/i.test(text);
194611
194685
  }
194612
194686
  function isKimiPlanTierError(text) {
194613
194687
  return /Your current (?:subscription does not have access to|plan supports only)\b/i.test(text);
@@ -194620,138 +194694,179 @@ var SUBSCRIPTION_CREDENTIALS = {
194620
194694
  CLAUDE_CODE_OAUTH_TOKEN: {
194621
194695
  label: "Claude Pro/Max subscription",
194622
194696
  command: "pullfrog auth claude",
194623
- docs: "https://docs.pullfrog.com/claude-auth"
194697
+ provider: "anthropic"
194624
194698
  },
194625
194699
  CODEX_AUTH_JSON: {
194626
194700
  label: "ChatGPT subscription",
194627
194701
  command: "pullfrog auth codex",
194628
- docs: "https://docs.pullfrog.com/codex-auth"
194702
+ provider: "openai"
194629
194703
  },
194630
194704
  GROK_AUTH_JSON: {
194631
194705
  label: "Grok subscription",
194632
194706
  command: "pullfrog auth grok",
194633
- docs: "https://docs.pullfrog.com/grok-auth"
194707
+ provider: "xai"
194634
194708
  }
194635
194709
  };
194710
+ function subscriptionFor(model) {
194711
+ const provider2 = model?.split("/")[0];
194712
+ for (const [env2, subscription] of Object.entries(SUBSCRIPTION_CREDENTIALS)) {
194713
+ if (subscription.provider === provider2) return { env: env2, ...subscription };
194714
+ }
194715
+ return void 0;
194716
+ }
194717
+ var SIGN_IN_AGAIN = (command) => `Sign in again by running \`${command}\` in a terminal`;
194718
+ var CLAUDE_ORG_DISABLED = {
194719
+ headline: "Your Claude organization doesn't allow subscription sign-in for Claude Code.",
194720
+ detail: "Ask its admin to allow it in the Claude Console, or connect an Anthropic API key instead."
194721
+ };
194722
+ function providerDisplayName(id) {
194723
+ for (const [key, provider2] of Object.entries(providers)) {
194724
+ if (key === id) return provider2.displayName;
194725
+ }
194726
+ return void 0;
194727
+ }
194636
194728
  function buildRejectedCredentialError(params) {
194637
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194638
194729
  const subscription = SUBSCRIPTION_CREDENTIALS[params.credential];
194639
- const detail = params.reason ? ` (\`${params.reason}\`)` : "";
194730
+ const details = params.reason ? { summary: "The provider's response", text: params.reason } : void 0;
194640
194731
  if (subscription) {
194641
- return [
194642
- `**Your ${subscription.label} ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
194643
- "",
194644
- `**To fix:** re-authenticate with \`${subscription.command}\`. A subscription credential can't be rotated from a provider dashboard \u2014 it has to be re-issued. You can also switch this repo to a model you hold an API key for.`,
194645
- "",
194646
- `[Re-authenticate \u2192](${subscription.docs}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194647
- ].join("\n");
194648
- }
194649
- const where = params.inPullfrogStore ? `[Update it in Pullfrog \u2192](${settingsUrl})` : `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)`;
194650
- const keysUrl = providerDashboard(params.credential)?.keys;
194651
- const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
194652
- return [
194653
- `**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
194654
- "",
194655
- `**To fix:** issue a new key in your provider dashboard and update the copy Pullfrog uses${params.inPullfrogStore ? " in the console" : " in your repo's GitHub Actions secrets"}.`,
194656
- "",
194657
- `${issue3}${where} \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194658
- ].join("\n");
194659
- }
194660
- function providerDashboard(credential) {
194732
+ const orgDisabled = params.reason !== void 0 && isClaudeSubscriptionDisabledError(params.reason);
194733
+ return renderFailure({
194734
+ headline: `Your ${subscription.label} ${CREDENTIAL_REJECTED_MARKER}.`,
194735
+ detail: orgDisabled ? `${CLAUDE_ORG_DISABLED.headline} ${CLAUDE_ORG_DISABLED.detail}` : `${SIGN_IN_AGAIN(subscription.command)}, or pick a model you have an API key for.`,
194736
+ details,
194737
+ links: [orgDisabled ? providerLink(params.owner) : pickModelLink(params)]
194738
+ });
194739
+ }
194740
+ const provider2 = credentialProvider(params.credential);
194741
+ const keysUrl = provider2 ? PROVIDER_DASHBOARDS[provider2.id]?.keys : void 0;
194742
+ const update = params.inPullfrogStore ? {
194743
+ label: "Update it in Pullfrog",
194744
+ url: `${getApiUrl()}/console/${params.owner}/${params.name}`
194745
+ } : { label: "Update the Actions secret", url: secretsUrl(params) };
194746
+ return renderFailure({
194747
+ headline: `Your ${provider2 ? `${provider2.displayName} key` : "key"} \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}.`,
194748
+ detail: `Issue a new key and update the copy ${params.inPullfrogStore ? "stored in Pullfrog" : "in this repo's GitHub Actions secrets"}.`,
194749
+ details,
194750
+ links: keysUrl ? [{ label: "Issue a new key", url: keysUrl }, update] : [update]
194751
+ });
194752
+ }
194753
+ function credentialProvider(credential) {
194661
194754
  for (const [id, provider2] of Object.entries(providers)) {
194662
- if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
194755
+ if (provider2.envVars.includes(credential)) return { id, displayName: provider2.displayName };
194663
194756
  }
194664
194757
  return void 0;
194665
194758
  }
194666
194759
  function buildCredentialPoolRefusedError(params) {
194667
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194668
- return [
194669
- `**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
194670
- "",
194671
- ...params.refused.map(
194672
- (credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
194673
- ),
194674
- "",
194675
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194676
- ].join("\n");
194760
+ const [first = "", second, ...rest] = params.refused.map(
194761
+ (credential) => describeRefusal({ credential, owner: params.owner, name: params.name })
194762
+ );
194763
+ return renderFailure({
194764
+ headline: `${CREDENTIAL_POOL_REFUSED_MARKER}.`,
194765
+ detail: second === void 0 ? first : `Every credential for \`${params.model}\` was turned down:`,
194766
+ list: second === void 0 ? void 0 : [first, second, ...rest],
194767
+ details: {
194768
+ summary: "The providers' responses",
194769
+ text: params.refused.map((credential) => `${credential.name}: ${credential.verdict.detail}`).join("\n")
194770
+ },
194771
+ links: [pickModelLink(params)]
194772
+ });
194677
194773
  }
194678
194774
  function describeRefusal(params) {
194679
194775
  const credential = params.credential;
194680
194776
  const verdict = credential.verdict;
194681
- const workflow = credential.source === "workflow";
194682
- const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
194683
194777
  const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
194684
- const dashboard = providerDashboard(credential.name);
194778
+ const dashboard = PROVIDER_DASHBOARDS[credentialProvider(credential.name)?.id ?? ""];
194779
+ const what = subscription ? `Your ${subscription.label}` : `Your \`${credential.name}\``;
194685
194780
  if (verdict.status === "exhausted" && subscription) {
194686
- const wait2 = verdict.resetAt ? `it resets at **${verdict.resetAt.toISOString().slice(0, 16).replace("T", " ")} UTC**; re-trigger after that` : "re-trigger after the limit resets";
194687
- return `${head} ${wait2}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
194781
+ const reset = verdict.resetAt ? `, which resets at **${verdict.resetAt.toISOString().slice(0, 16).replace("T", " ")} UTC**` : "";
194782
+ return `${what} has hit its usage limit${reset}. Re-trigger after it resets, or add an API key for this provider, which Pullfrog falls back to automatically.`;
194783
+ }
194784
+ if (verdict.status === "exhausted") {
194785
+ return `${what} is out of credit or quota. ${dashboard ? `[Add credit](${dashboard.billing})` : "Add credit"} with the provider, or raise its quota.`;
194688
194786
  }
194689
- if (verdict.status === "exhausted")
194690
- return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
194691
- if (subscription)
194692
- return `${head} re-authenticate with \`${subscription.command}\`, or switch this repo to a model you hold an API key for. [Re-authenticate \u2192](${subscription.docs})`;
194693
- const where = workflow ? `[Update the GitHub Actions secret \u2192](https://github.com/${params.owner}/${params.name}/settings/secrets/actions)` : `[Update it in Pullfrog \u2192](${params.settingsUrl})`;
194694
- const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
194695
- return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
194787
+ if (subscription && isClaudeSubscriptionDisabledError(verdict.detail)) {
194788
+ return `${CLAUDE_ORG_DISABLED.headline} ${CLAUDE_ORG_DISABLED.detail}`;
194789
+ }
194790
+ if (subscription) return `${what} was rejected. ${SIGN_IN_AGAIN(subscription.command)}.`;
194791
+ const issue3 = dashboard?.keys ? `[Issue a new key](${dashboard.keys})` : "Issue a new key";
194792
+ const where = credential.source === "workflow" ? `in [this repo's GitHub Actions secrets](${secretsUrl(params)})` : `in [Pullfrog](${getApiUrl()}/console/${params.owner}#billing)`;
194793
+ return `${what} was rejected. ${issue3} and update it ${where}.`;
194696
194794
  }
194697
194795
  function formatApiKeyErrorSummary(params) {
194698
194796
  if (params.raw.includes(MISSING_KEY_MARKER)) {
194699
- if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
194797
+ if (params.raw.startsWith(`**${MISSING_KEY_MARKER}`)) return params.raw;
194700
194798
  return buildMissingApiKeyError({ owner: params.owner, name: params.name });
194701
194799
  }
194702
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
194703
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
194800
+ const details = params.output ? { summary: "The provider's response", text: params.output } : void 0;
194801
+ const subscription = subscriptionFor(params.model);
194704
194802
  if (isClaudeSessionLimitError(params.raw)) {
194705
194803
  const reset = CLAUDE_SESSION_LIMIT_PATTERN.exec(params.raw)?.[1]?.trim();
194706
- const resets = reset ? ` It resets at **${reset}**.` : "";
194707
- return [
194708
- `**Your Claude subscription has hit its usage limit.**${resets} Re-trigger Pullfrog after the reset, or add an \`ANTHROPIC_API_KEY\` repo secret \u2014 Pullfrog routes around an exhausted subscription automatically when one is present.`,
194709
- "",
194710
- `[Add repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194711
- ].join("\n");
194804
+ return renderFailure({
194805
+ headline: "Your Claude Pro/Max subscription has hit its usage limit.",
194806
+ detail: `${reset ? `It resets at **${reset}**. ` : ""}Re-trigger after that, or connect an Anthropic API key, which Pullfrog falls back to automatically.`,
194807
+ details,
194808
+ links: [providerLink(params.owner)]
194809
+ });
194712
194810
  }
194713
194811
  if (isMalformedKeyError(params.raw)) {
194714
- return [
194715
- "**Your stored API key can't be sent as an HTTP header.** It contains a line break or control character \u2014 usually a key pasted across several lines. Re-save it as a single line; the key itself is likely fine.",
194716
- "",
194717
- `[Pullfrog secrets \u2192](${getApiUrl()}/console/${params.owner}) \xB7 [Repo secrets \u2192](${githubSecretsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194718
- ].join("\n");
194812
+ return renderFailure({
194813
+ headline: "Your stored API key can't be sent as an HTTP header.",
194814
+ detail: "It contains a line break or control character, usually from a key pasted across several lines. Re-save it as a single line; the key itself is likely fine.",
194815
+ links: [
194816
+ {
194817
+ label: "Pullfrog provider settings",
194818
+ url: `${getApiUrl()}/console/${params.owner}#billing`
194819
+ },
194820
+ { label: "GitHub Actions secrets", url: secretsUrl(params) }
194821
+ ]
194822
+ });
194719
194823
  }
194720
194824
  if (isClaudeSubscriptionDisabledError(params.raw)) {
194721
- return [
194722
- `**Your organization has disabled Claude subscription access for Claude Code.** Ask your Claude organization's admin to re-enable it in the Claude Console, or set an \`ANTHROPIC_API_KEY\` for this repo instead, then re-trigger the run.`,
194723
- "",
194724
- `[Add repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194725
- ].join("\n");
194825
+ return renderFailure({
194826
+ ...CLAUDE_ORG_DISABLED,
194827
+ details,
194828
+ links: [providerLink(params.owner)]
194829
+ });
194726
194830
  }
194727
194831
  if (isKimiPlanTierError(params.raw)) {
194728
- return [
194729
- "**Your Kimi membership doesn't include the model this run asked for.** Kimi K3 needs a Moderato plan or above, and K3's 1M context and Kimi K2 HighSpeed need Allegretto or above. Upgrade the membership, or switch this repo to **Kimi K2**, which every tier includes.",
194730
- "",
194731
- `[Kimi plans \u2192](https://www.kimi.com/membership/pricing) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/kimi-code) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194732
- ].join("\n");
194832
+ return renderFailure({
194833
+ headline: "Your Kimi membership doesn't include the model this run asked for.",
194834
+ detail: "Kimi K3 needs a Moderato plan or above, and K3's 1M context and Kimi K2 HighSpeed need Allegretto or above. Upgrade the membership, or switch this repo to **Kimi K2**, which every tier includes.",
194835
+ details,
194836
+ links: [
194837
+ { label: "Kimi plans", url: "https://www.kimi.com/membership/pricing" },
194838
+ pickModelLink(params)
194839
+ ]
194840
+ });
194733
194841
  }
194734
194842
  if (isOAuthCredentialExpiredError(params.raw)) {
194735
- return [
194736
- `**Your provider OAuth credential has expired or been revoked.** Re-authenticate the provider connection (e.g. \`pullfrog auth claude\` / \`pullfrog auth codex\` / \`pullfrog auth grok\`), then re-trigger the run.`,
194737
- "",
194738
- `[Claude subscription \u2192](https://docs.pullfrog.com/claude-auth) \xB7 [ChatGPT subscription \u2192](https://docs.pullfrog.com/codex-auth) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194739
- ].join("\n");
194740
- }
194741
- const subscription = Object.keys(SUBSCRIPTION_CREDENTIALS).find(hasEnvVar2);
194742
- if (subscription && !hasEnvVar2("ANTHROPIC_API_KEY")) {
194743
- const details = SUBSCRIPTION_CREDENTIALS[subscription];
194744
- return [
194745
- `**Your ${details?.label} was rejected during this run.** Re-authenticate with \`${details?.command}\` and re-trigger \u2014 a subscription credential can't be rotated from a provider dashboard.`,
194746
- "",
194747
- `[Re-authenticate \u2192](${details?.docs}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194748
- ].join("\n");
194749
- }
194750
- return [
194751
- `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the copy Pullfrog uses \u2014 wherever you saved it.`,
194752
- "",
194753
- `[Pullfrog secrets \u2192](${getApiUrl()}/console/${params.owner}) \xB7 [Repo secrets \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
194754
- ].join("\n");
194843
+ return renderFailure({
194844
+ headline: subscription ? `Your ${subscription.label}'s sign-in has expired or been revoked.` : "Your provider sign-in has expired or been revoked.",
194845
+ detail: subscription ? `${SIGN_IN_AGAIN(subscription.command)}, then re-trigger.` : "Sign in again with `pullfrog auth claude`, `pullfrog auth codex` or `pullfrog auth grok` in a terminal, whichever this repo uses, then re-trigger.",
194846
+ details,
194847
+ links: [pickModelLink(params)]
194848
+ });
194849
+ }
194850
+ if (subscription && hasEnvVar2(subscription.env) && params.credential !== "api_key") {
194851
+ return renderFailure({
194852
+ headline: `Your ${subscription.label} was rejected during this run.`,
194853
+ detail: `${SIGN_IN_AGAIN(subscription.command)}, then re-trigger.`,
194854
+ details,
194855
+ links: [pickModelLink(params)]
194856
+ });
194857
+ }
194858
+ return renderFailure({
194859
+ headline: `Your ${providerDisplayName(params.model?.split("/")[0]) ?? "model provider"} API key was rejected.`,
194860
+ detail: "Issue a new key with the provider, then update the copy Pullfrog uses, in Pullfrog or in this repo's GitHub Actions secrets.",
194861
+ details,
194862
+ links: [
194863
+ {
194864
+ label: "Pullfrog provider settings",
194865
+ url: `${getApiUrl()}/console/${params.owner}#billing`
194866
+ },
194867
+ { label: "GitHub Actions secrets", url: secretsUrl(params) }
194868
+ ]
194869
+ });
194755
194870
  }
194756
194871
 
194757
194872
  // utils/agent.ts
@@ -194777,7 +194892,7 @@ function resolveSlug(slug2) {
194777
194892
  const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
194778
194893
  if (!bedrockId) {
194779
194894
  throw new Error(
194780
- `${BEDROCK_MODEL_ID_ENV} ${BYOK_SLUG_MARKER} "${slug2}". set it to an AWS Bedrock model ID from the Bedrock console. see https://docs.pullfrog.com/bedrock for setup.`
194895
+ `\`${BEDROCK_MODEL_ID_ENV}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it in the workflow's \`env:\` block to a Bedrock model ID from the AWS console. [Amazon Bedrock setup guide](https://docs.pullfrog.com/bedrock)`
194781
194896
  );
194782
194897
  }
194783
194898
  return bedrockId;
@@ -194786,7 +194901,7 @@ function resolveSlug(slug2) {
194786
194901
  const vertexId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
194787
194902
  if (!vertexId) {
194788
194903
  throw new Error(
194789
- `${VERTEX_MODEL_ID_ENV} ${BYOK_SLUG_MARKER} "${slug2}". set it to a Google Vertex AI model ID from Model Garden. see https://docs.pullfrog.com/vertex for setup.`
194904
+ `\`${VERTEX_MODEL_ID_ENV}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it in the workflow's \`env:\` block to a Vertex AI model ID from Model Garden. [Google Vertex AI setup guide](https://docs.pullfrog.com/vertex)`
194790
194905
  );
194791
194906
  }
194792
194907
  return vertexId;
@@ -194795,7 +194910,7 @@ function resolveSlug(slug2) {
194795
194910
  const deployment = process.env[AZURE_DEPLOYMENT_ENV]?.trim();
194796
194911
  if (!deployment) {
194797
194912
  throw new Error(
194798
- `${AZURE_DEPLOYMENT_ENV} ${BYOK_SLUG_MARKER} "${slug2}". set it to the name of your Azure OpenAI deployment (not the model it serves \u2014 Azure routes on the deployment name). see https://docs.pullfrog.com/azure for setup.`
194913
+ `\`${AZURE_DEPLOYMENT_ENV}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it to your deployment's name, not the model it serves, on the Azure OpenAI provider in Pullfrog or in the workflow's \`env:\` block. [Azure OpenAI setup guide](https://docs.pullfrog.com/azure)`
194799
194914
  );
194800
194915
  }
194801
194916
  return `${AZURE_PROVIDER}/${deployment}`;
@@ -194804,7 +194919,7 @@ function resolveSlug(slug2) {
194804
194919
  const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
194805
194920
  if (!modelId) {
194806
194921
  throw new Error(
194807
- `${OPENAI_COMPATIBLE_MODEL_ENV} ${BYOK_SLUG_MARKER} "${slug2}". set it to the model ID served by your OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway or DashScope model). see https://docs.pullfrog.com/openai-compatible for setup.`
194922
+ `\`${OPENAI_COMPATIBLE_MODEL_ENV}\` ${BYOK_SLUG_MARKER} \`${slug2}\`. Set it to the model ID your endpoint serves, on the OpenAI-compatible provider in Pullfrog or in the workflow's \`env:\` block. [OpenAI-compatible setup guide](https://docs.pullfrog.com/openai-compatible)`
194808
194923
  );
194809
194924
  }
194810
194925
  return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
@@ -194876,9 +194991,12 @@ var BillingError = class extends Error {
194876
194991
  }
194877
194992
  };
194878
194993
  var TransientError = class extends Error {
194879
- constructor(message2) {
194994
+ /** the customer-facing follow-up to `message`, which is the headline. */
194995
+ detail;
194996
+ constructor(message2, detail = "This is on our side, and nothing was billed. The next run should succeed; if it keeps failing, check [status.pullfrog.com](https://status.pullfrog.com).") {
194880
194997
  super(message2);
194881
194998
  this.name = "TransientError";
194999
+ this.detail = detail;
194882
195000
  }
194883
195001
  };
194884
195002
  function billingConsoleUrl(owner) {
@@ -194913,41 +195031,34 @@ function formatCommercialGateSummary(params) {
194913
195031
  });
194914
195032
  }
194915
195033
  function formatBillingErrorSummary(error49, owner) {
195034
+ const billing = { label: "Add credit", url: billingConsoleUrl(owner) };
194916
195035
  if (error49.code === "router_requires_card") {
194917
- return [
194918
- "**Your Pullfrog Router balance is empty.**",
194919
- "",
194920
- "Add a card to top up your Router balance, or bring your own key. Router usage is billed at provider cost with no platform markup.",
194921
- "",
194922
- `[Add a card to top up \u2192](${billingConsoleUrl(owner)}) \xB7 [Bring your own key \u2192](${billingConsoleUrl(owner)})`
194923
- ].join("\n");
195036
+ return renderFailure({
195037
+ headline: "Your Pullfrog Router balance is empty.",
195038
+ detail: "Add a card to top it up, or connect your own provider key. Router usage is billed at provider cost with no markup.",
195039
+ links: [{ label: "Billing and providers", url: billingConsoleUrl(owner) }]
195040
+ });
194924
195041
  }
194925
195042
  if (error49.code === "router_balance_exhausted") {
194926
- return [
194927
- "**Your Pullfrog Router balance is exhausted.**",
194928
- "",
194929
- "You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
194930
- "",
194931
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner)})`
194932
- ].join("\n");
195043
+ return renderFailure({
195044
+ headline: "Your Pullfrog Router balance is used up.",
195045
+ detail: "Auto-reload is off, so runs paused once the balance passed its overdraft buffer. Add credit or turn on auto-reload.",
195046
+ links: [billing]
195047
+ });
194933
195048
  }
194934
195049
  if (error49.code === "router_keylimit_exhausted") {
194935
- return [
194936
- "**This run was cut short: your Pullfrog Router balance ran out mid-run.**",
194937
- "",
194938
- "OpenRouter stopped the agent because the per-run budget was exhausted. Your wallet is now negative; top up or enable auto-reload to keep runs flowing.",
194939
- "",
194940
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner)})`
194941
- ].join("\n");
195050
+ return renderFailure({
195051
+ headline: "Your Pullfrog Router balance ran out partway through this run.",
195052
+ detail: "Add credit or turn on auto-reload to keep runs going.",
195053
+ links: [billing]
195054
+ });
194942
195055
  }
194943
195056
  if (error49.code === "router_monthly_limit") {
194944
- return [
194945
- "**Pullfrog Router is out of credit, and the monthly spend limit blocks the next auto-reload.**",
194946
- "",
194947
- "Top up by hand, ask your admin to raise the cap, or wait for it to reset at 00:00 UTC on the 1st.",
194948
- "",
194949
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Adjust limit \u2192](${billingConsoleUrl(owner)})`
194950
- ].join("\n");
195057
+ return renderFailure({
195058
+ headline: "Your Pullfrog Router balance is empty, and the monthly spend limit blocks the next auto-reload.",
195059
+ detail: "Add credit by hand, ask an org owner to raise the limit, or wait for it to reset at 00:00 UTC on the 1st.",
195060
+ links: [billing]
195061
+ });
194951
195062
  }
194952
195063
  if (error49.code === "commercial_plan_required") {
194953
195064
  return formatCommercialGateSummary({
@@ -194969,39 +195080,27 @@ function formatBillingErrorSummary(error49, owner) {
194969
195080
  }
194970
195081
  if (error49.needsReauthentication) {
194971
195082
  const code = error49.declineCode ?? "authentication_required";
194972
- return [
194973
- `**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
194974
- "",
194975
- "Pullfrog can't complete a 3DS challenge from inside a workflow. Top up your Router balance once in Stripe Checkout, subsequent runs draw from the prepaid balance without re-triggering 3DS.",
194976
- "",
194977
- `[Top up balance \u2192](${billingConsoleUrl(owner)})`
194978
- ].join("\n");
195083
+ return renderFailure({
195084
+ headline: `Your card issuer requires 3D Secure on every charge (\`${code}\`).`,
195085
+ detail: "Pullfrog can't complete a 3D Secure check from inside a workflow. Add credit once by hand, and later runs draw on that balance without another check.",
195086
+ links: [billing]
195087
+ });
194979
195088
  }
194980
195089
  if (error49.declineCode) {
194981
- return [
194982
- `**Your card was declined** (\`${error49.declineCode}\`).`,
194983
- "",
194984
- "Update your payment method and Pullfrog will retry on the next run.",
194985
- "",
194986
- `[Update payment method \u2192](${billingConsoleUrl(owner)})`
194987
- ].join("\n");
195090
+ return renderFailure({
195091
+ headline: `Your card was declined (\`${error49.declineCode}\`).`,
195092
+ detail: "Update your payment method, and Pullfrog will retry on the next run.",
195093
+ links: [{ label: "Update payment method", url: billingConsoleUrl(owner) }]
195094
+ });
194988
195095
  }
194989
- return [
194990
- "**Your Pullfrog balance is empty.**",
194991
- "",
194992
- "Top up your balance or enable auto-reload to keep runs flowing.",
194993
- "",
194994
- `[Manage billing \u2192](${billingConsoleUrl(owner)})`
194995
- ].join("\n");
195096
+ return renderFailure({
195097
+ headline: "Your Pullfrog balance is empty.",
195098
+ detail: "Add credit or turn on auto-reload to keep runs going.",
195099
+ links: [billing]
195100
+ });
194996
195101
  }
194997
- function formatTransientErrorSummary(error49, owner) {
194998
- return [
194999
- "**Pullfrog billing is temporarily unavailable.**",
195000
- "",
195001
- error49.message,
195002
- "",
195003
- `Usually transient; the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com) or [your console](${billingConsoleUrl(owner)}).`
195004
- ].join("\n");
195102
+ function formatTransientErrorSummary(error49) {
195103
+ return renderFailure({ headline: error49.message, detail: error49.detail });
195005
195104
  }
195006
195105
 
195007
195106
  // utils/credentialCheck.ts
@@ -195374,10 +195473,7 @@ var select = mutation({
195374
195473
  signal: ctx.signal
195375
195474
  });
195376
195475
  if (response.status === 404) return null;
195377
- if (!response.ok)
195378
- throw new Error("could not load a configured credential; no fallback was charged", {
195379
- cause: response.status
195380
- });
195476
+ if (!response.ok) throw new Error(buildSecretsUnavailableError(), { cause: response.status });
195381
195477
  return selectedCredentialSchema.parse(await response.json());
195382
195478
  },
195383
195479
  retry: (error49, attempt) => error49 instanceof Error && typeof error49.cause === "number" && error49.cause >= 400 && error49.cause < 500 ? -1 : delay2([250, 1e3], attempt),
@@ -196191,26 +196287,16 @@ function ossAllowedLabels() {
196191
196287
  return ossFundedModelNames().map((name) => `\`${name}\``).join(", ");
196192
196288
  }
196193
196289
  function buildModelAccessError(input) {
196194
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
196195
- const secretsUrl = `https://github.com/${input.owner}/${input.name}/settings/secrets/actions`;
196196
- const docsUrl = "https://docs.pullfrog.com/keys";
196197
- const headline = `**The ${MODEL_ACCESS_MARKER}: \`${input.model}\`.**`;
196198
- const branch = {
196199
- oss: {
196200
- why: `This repo runs on Pullfrog's OSS subsidy, which funds a set of pre-approved models: ${ossAllowedLabels()}. Pick one of those, or add your own provider key to run a different one.`,
196201
- cta: `[Configure model \u2192](${settingsUrl}) \xB7 [Add a provider key \u2192](${secretsUrl}) \xB7 [Setup docs \u2192](${docsUrl})`
196202
- },
196203
- byok_no_key: {
196204
- why: `No provider key for \`${input.model}\` is present in this run's environment. Add the matching provider key as a GitHub Actions secret, or pick a model you already have a key for.`,
196205
- cta: `[Open repo secrets \u2192](${secretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](${docsUrl})`
196206
- },
196207
- router: {
196208
- why: "Pullfrog Router can't serve this model. Pick a Router-supported model, or add your own provider key to run it directly.",
196209
- cta: `[Configure model \u2192](${settingsUrl}) \xB7 [Add a provider key \u2192](${secretsUrl})`
196210
- }
196290
+ const why = {
196291
+ oss: `This repo runs on Pullfrog's OSS subsidy, which funds a set of pre-approved models: ${ossAllowedLabels()}. Pick one of those, or connect your own provider to run a different one.`,
196292
+ byok_no_key: `This run has no API key for \`${input.model}\`. Connect one, or pick a model you already have a key for.`,
196293
+ router: "Pullfrog Router can't serve this model. Pick a Router-supported model, or connect your own provider to run it directly."
196211
196294
  };
196212
- const { why, cta } = branch[input.reason];
196213
- return [headline, "", why, "", cta].join("\n");
196295
+ return renderFailure({
196296
+ headline: `The ${MODEL_ACCESS_MARKER}: \`${input.model}\`.`,
196297
+ detail: why[input.reason],
196298
+ links: [pickModelLink(input), providerLink(input.owner)]
196299
+ });
196214
196300
  }
196215
196301
 
196216
196302
  // utils/overrides.ts
@@ -196767,9 +196853,6 @@ async function revokeGitHubInstallationToken(token) {
196767
196853
 
196768
196854
  // utils/errorReport.ts
196769
196855
  async function reportErrorToComment(ctx) {
196770
- const formattedError = ctx.title ? `${ctx.title}
196771
-
196772
- ${ctx.error}` : ctx.error;
196773
196856
  const repoContext = parseRepoContext();
196774
196857
  const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
196775
196858
  const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
@@ -196788,9 +196871,10 @@ ${ctx.error}` : ctx.error;
196788
196871
  clamped: ctx.toolState.modelClamped,
196789
196872
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
196790
196873
  oss: ctx.toolState.oss,
196791
- owner: repoContext.owner
196874
+ owner: repoContext.owner,
196875
+ failed: true
196792
196876
  });
196793
- const body = `${formattedError}${footer}`;
196877
+ const body = `${redactSecrets(ctx.error)}${footer}`;
196794
196878
  const comment = ctx.toolState.progressComment;
196795
196879
  if (comment) {
196796
196880
  await updateProgressComment(
@@ -196830,8 +196914,9 @@ async function mintProxyKey(ctx) {
196830
196914
  const r = await apiFetch({ path: "/api/proxy-token", method: "POST", headers });
196831
196915
  if (r.status >= 500) {
196832
196916
  const body = await r.json().catch(() => null);
196917
+ log.warning(`proxy key mint: ${r.status} ${body?.error ?? ""}`);
196833
196918
  throw new TransientError(
196834
- body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
196919
+ "Pullfrog's billing service is temporarily unavailable, so this run didn't start."
196835
196920
  );
196836
196921
  }
196837
196922
  return r;
@@ -196849,7 +196934,8 @@ async function mintProxyKey(ctx) {
196849
196934
  }
196850
196935
  if (response.status === 404) {
196851
196936
  throw new TransientError(
196852
- "Pullfrog couldn't match this repository to its account \u2014 it may have just been renamed or transferred. The link refreshes automatically on the next run; if it keeps failing, reinstall the GitHub App on the repo's current owner."
196937
+ "Pullfrog couldn't match this repository to its account.",
196938
+ "It may have just been renamed or transferred, and the link refreshes on the next run. If it keeps failing, reinstall the GitHub App on the repository's current owner."
196853
196939
  );
196854
196940
  }
196855
196941
  if (!response.ok) {
@@ -196958,7 +197044,7 @@ async function runProxyResolution(ctx) {
196958
197044
  throw error49;
196959
197045
  }
196960
197046
  if (error49 instanceof TransientError) {
196961
- const summary3 = formatTransientErrorSummary(error49, ctx.repo.owner);
197047
+ const summary3 = formatTransientErrorSummary(error49);
196962
197048
  await writeSummary(summary3).catch(() => {
196963
197049
  });
196964
197050
  await reportErrorToComment({
@@ -196983,13 +197069,14 @@ async function resolveTrialFallback(ctx) {
196983
197069
  if (!key) return false;
196984
197070
  process.env.OPENROUTER_API_KEY = key;
196985
197071
  maskSecret(key);
196986
- ctx.payload.proxyModel = DEFAULT_PROXY_MODEL;
196987
- ctx.toolState.model = DEFAULT_PROXY_MODEL;
197072
+ ctx.payload.proxyModel = TRIAL_PROXY_MODEL;
197073
+ ctx.toolState.model = TRIAL_PROXY_MODEL;
196988
197074
  ctx.toolState.modelClamped = {
196989
- from: ctx.configuredModel ?? DEFAULT_PROXY_MODEL,
196990
- reason: "trial"
197075
+ from: ctx.configuredModel ?? TRIAL_PROXY_MODEL,
197076
+ reason: "trial",
197077
+ creditsLeftUsd: ctx.creditsLeftUsd
196991
197078
  };
196992
- log.info(`\xBB proxy: trial \u2192 ${DEFAULT_PROXY_MODEL}`);
197079
+ log.info(`\xBB proxy: trial \u2192 ${TRIAL_PROXY_MODEL}`);
196993
197080
  return true;
196994
197081
  }
196995
197082
 
@@ -197102,14 +197189,27 @@ ${collapsible}` : output;
197102
197189
  return { success: true, output: ctx.result.output || "" };
197103
197190
  } catch (writeError) {
197104
197191
  const error50 = `failed to deliver agent result: ${getErrorMessage2(writeError)}`;
197105
- await reportErrorToComment({ toolState, error: error50, title: "Error" }).catch(() => {
197192
+ await reportErrorToComment({
197193
+ toolState,
197194
+ error: renderFailure({
197195
+ headline: "Pullfrog couldn't post its answer to GitHub.",
197196
+ detail: "GitHub refused the write; its error is below. Re-trigger to try again.",
197197
+ error: getErrorMessage2(writeError)
197198
+ })
197199
+ }).catch(() => {
197106
197200
  });
197107
197201
  return { success: false, error: error50, output: ctx.result.output || "" };
197108
197202
  }
197109
197203
  }
197110
197204
  const error49 = ctx.result.error || "agent completed without reporting progress";
197111
197205
  try {
197112
- await reportErrorToComment({ toolState, error: error49, title: "Error" });
197206
+ await reportErrorToComment({
197207
+ toolState,
197208
+ error: renderFailure({
197209
+ headline: "The agent finished without posting anything.",
197210
+ detail: "Re-trigger to try again."
197211
+ })
197212
+ });
197113
197213
  } catch {
197114
197214
  }
197115
197215
  return { success: false, error: error49, output: ctx.result.output || "" };
@@ -197170,63 +197270,57 @@ async function resolveRunContextData(params) {
197170
197270
  // outcome as the server-side failure: the run never sees stored secrets.
197171
197271
  secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0,
197172
197272
  routerUnfunded: runContext.routerUnfunded,
197173
- trialFallback: runContext.trialFallback
197273
+ trialFallback: runContext.trialFallback,
197274
+ trialCreditsLeftUsd: runContext.trialCreditsLeftUsd
197174
197275
  };
197175
197276
  }
197176
197277
 
197177
197278
  // utils/agentHangReport.ts
197178
197279
  var MAX_STDERR_BYTES = 3e3;
197179
197280
  function formatAgentHangBody(input) {
197180
- if (!input.diagnostic) return null;
197181
- if (input.diagnostic.lastProviderError === "provider billing exhausted") {
197182
- return formatBillingExhaustedBody(input.diagnostic);
197183
- }
197184
- const verb = input.isHang ? "stalled" : "failed";
197185
- const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
197186
- const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
197187
- const explanation = formatExplanation({
197188
- isHang: input.isHang,
197189
- errorMessage: input.errorMessage,
197190
- idleSec: input.diagnostic.idleSec
197191
- });
197192
- const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
197193
- const tail = renderStderrTail(input.diagnostic.recentStderr);
197194
- if (tail) {
197195
- const fence = pickFence(tail);
197196
- parts.push(
197197
- "",
197198
- "<details><summary>Recent agent stderr</summary>",
197199
- "",
197200
- fence,
197201
- tail,
197202
- fence,
197203
- "",
197204
- "</details>"
197205
- );
197281
+ const diagnostic = input.diagnostic;
197282
+ if (!diagnostic) return null;
197283
+ const output = renderStderrTail(diagnostic.recentStderr);
197284
+ const details = output ? { summary: "Last agent output", text: output } : void 0;
197285
+ const idle = formatIdle(diagnostic.idleSec ?? parseIdleSec(input.errorMessage));
197286
+ if (diagnostic.lastProviderError === PROVIDER_BILLING_EXHAUSTED_LABEL) {
197287
+ const billingUrl = extractBillingUrl(diagnostic.recentStderr);
197288
+ return renderFailure({
197289
+ headline: "Your model provider's account is out of credit.",
197290
+ detail: "The provider kept refusing the request, so Pullfrog stopped the run. Top up the account, then re-trigger.",
197291
+ details,
197292
+ links: billingUrl ? [{ label: "Top up", url: billingUrl }] : []
197293
+ });
197206
197294
  }
197207
- return parts.join("\n");
197208
- }
197209
- function formatExplanation(input) {
197210
- if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
197211
- const idleSec = input.idleSec ?? parseIdleSec(input.errorMessage);
197212
- if (idleSec === void 0) {
197213
- return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
197295
+ if (diagnostic.lastProviderError) {
197296
+ return renderFailure({
197297
+ headline: "The model provider kept returning errors, so Pullfrog stopped this run.",
197298
+ detail: `The last one was \`${diagnostic.lastProviderError}\`. Re-trigger to try again.`,
197299
+ details
197300
+ });
197301
+ }
197302
+ if (!diagnostic.sawModelOutput) {
197303
+ return renderFailure({
197304
+ headline: `The model never responded, so Pullfrog stopped this run${idle ? ` after ${idle}` : ""}.`,
197305
+ detail: "The request went out but nothing came back. This is usually temporary, so re-trigger to try again.",
197306
+ details
197307
+ });
197214
197308
  }
197215
- return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
197309
+ return renderFailure({
197310
+ headline: `The agent went quiet${idle ? ` for ${idle}` : ""}, so Pullfrog stopped this run.`,
197311
+ detail: "Re-trigger to try again.",
197312
+ details
197313
+ });
197314
+ }
197315
+ function formatIdle(seconds) {
197316
+ if (seconds === void 0) return void 0;
197317
+ if (seconds < 90) return `${seconds} seconds`;
197318
+ return `${Math.round(seconds / 60)} minutes`;
197216
197319
  }
197217
197320
  function parseIdleSec(message2) {
197218
197321
  const match3 = /no output for (\d+)s/.exec(message2);
197219
197322
  return match3 ? Number(match3[1]) : void 0;
197220
197323
  }
197221
- function formatEventsPart(diagnostic) {
197222
- if (diagnostic.lastProviderError) {
197223
- return `${diagnostic.eventCount} events were processed before the failure.`;
197224
- }
197225
- if (!diagnostic.sawModelOutput) {
197226
- return "The model produced no output at all before the stall \u2014 the request was sent but nothing came back. This is usually transient; re-running often succeeds.";
197227
- }
197228
- return `${diagnostic.eventCount} events were processed before the failure.`;
197229
- }
197230
197324
  function renderStderrTail(lines) {
197231
197325
  if (lines.length === 0) return "";
197232
197326
  const joined = lines.join("\n");
@@ -197234,13 +197328,6 @@ function renderStderrTail(lines) {
197234
197328
  return `... (older lines truncated)
197235
197329
  ${joined.slice(-MAX_STDERR_BYTES)}`;
197236
197330
  }
197237
- function pickFence(content) {
197238
- let max = 0;
197239
- for (const match3 of content.matchAll(/`+/g)) {
197240
- if (match3[0].length > max) max = match3[0].length;
197241
- }
197242
- return "`".repeat(Math.max(3, max + 1));
197243
- }
197244
197331
  function extractBillingUrl(lines) {
197245
197332
  const urlPattern = /https:\/\/(?:opencode\.ai\/[^\s"]*billing[^\s"]*|console\.anthropic\.com[^\s"]*|console\.cloud\.google\.com[^\s"]*billing[^\s"]*)/i;
197246
197333
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -197249,28 +197336,6 @@ function extractBillingUrl(lines) {
197249
197336
  }
197250
197337
  return void 0;
197251
197338
  }
197252
- function formatBillingExhaustedBody(diagnostic) {
197253
- const headline = `**${diagnostic.label} stopped** \u2014 your model provider returned a billing-exhausted response.`;
197254
- const billingUrl = extractBillingUrl(diagnostic.recentStderr);
197255
- const cta = billingUrl ? `Top up your provider balance, then re-run: [${billingUrl}](${billingUrl})` : "Top up your model-provider balance (or rotate to a key with remaining credits) and re-run.";
197256
- const explanation = "The agent kept retrying the request because the provider marked the failure as transient. Pullfrog's activity-timeout watchdog ended the run after no further events were emitted.";
197257
- const parts = [headline, "", explanation, "", cta];
197258
- const tail = renderStderrTail(diagnostic.recentStderr);
197259
- if (tail) {
197260
- const fence = pickFence(tail);
197261
- parts.push(
197262
- "",
197263
- "<details><summary>Recent agent stderr</summary>",
197264
- "",
197265
- fence,
197266
- tail,
197267
- fence,
197268
- "",
197269
- "</details>"
197270
- );
197271
- }
197272
- return parts.join("\n");
197273
- }
197274
197339
 
197275
197340
  // utils/runErrorRenderer.ts
197276
197341
  function isProviderModelNotFoundError(message2) {
@@ -197285,84 +197350,9 @@ function isNoProviderAvailableError(message2) {
197285
197350
  function isZenFreeTierGateError(message2) {
197286
197351
  return /free tier can only be used from within OpenCode/i.test(message2);
197287
197352
  }
197288
- function formatZenFreeTierGateSummary(input) {
197289
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
197290
- return [
197291
- "**Zen's free tier refused a request this run made.** OpenCode Zen currently rejects free-model requests whose tool list is missing one of the built-in tools it expects ([upstream issue](https://github.com/anomalyco/opencode/issues/49587)). Pullfrog sends all of them, but opencode's mid-run context-compaction request carries no tools at all, and a repo-level `opencode.json` that denies `bash`, `glob`, `grep` or `read` strips that tool from every request.",
197292
- "",
197293
- 'Pick a paid model for this repo, drop any `"deny"` on `bash`, `glob`, `grep` or `read` from the repo\'s `opencode.json` if it sets one, or split the PR so the run stays under the compaction threshold and re-trigger.',
197294
- "",
197295
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
197296
- "",
197297
- `\`\`\`
197298
- ${input.raw}
197299
- \`\`\``
197300
- ].join("\n");
197301
- }
197302
- function formatNoProviderAvailableSummary(input) {
197303
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
197304
- return [
197305
- "**The provider refused to serve this model.** It's listed in the catalog, but the account this run used has no access to it \u2014 so the request was accepted and then declined.",
197306
- "",
197307
- "This often happens when Pullfrog auto-selects a model your account can't reach. Pin an explicit model for this repo, or add credentials for the provider that was picked.",
197308
- "",
197309
- `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
197310
- "",
197311
- `\`\`\`
197312
- ${input.raw}
197313
- \`\`\``
197314
- ].join("\n");
197315
- }
197316
- function isRunTimeoutError(message2) {
197317
- return message2.startsWith("agent run timed out after");
197318
- }
197319
- function formatRunTimeoutSummary(input) {
197320
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
197321
- return [
197322
- "**This run hit its time limit while still working, so it stopped before finishing.**",
197323
- "",
197324
- "Give runs longer with `--timeout=2h` (or `--notimeout`) in this repo's instructions \u2014 or raise the workflow's `timeout` input if it sets one, since that input wins. A smaller PR or a lower effort also finishes sooner.",
197325
- "",
197326
- `[Repo settings \u2192](${settingsUrl}) \xB7 [Flags \u2192](https://docs.pullfrog.com/flags)`,
197327
- "",
197328
- `\`\`\`
197329
- ${input.raw}
197330
- \`\`\``
197331
- ].join("\n");
197332
- }
197333
- function formatContextOverflowSummary(input) {
197334
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
197335
- return [
197336
- "**This run exceeded the model's context window.** Pullfrog read more than the model could hold, so it stopped before finishing.",
197337
- "",
197338
- `Pick a model with a larger context window, or split this PR into smaller ones and re-trigger.`,
197339
- "",
197340
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
197341
- "",
197342
- `\`\`\`
197343
- ${input.raw}
197344
- \`\`\``
197345
- ].join("\n");
197346
- }
197347
- function formatGenericFailure(errorMessage) {
197348
- return [
197349
- "Pullfrog ran into an unexpected error and couldn't finish this run. The underlying error is below \u2014 re-trigger Pullfrog to try again, and reach out to support if it keeps happening.",
197350
- "",
197351
- "```",
197352
- errorMessage,
197353
- "```"
197354
- ].join("\n");
197355
- }
197356
- function formatMinimalFailureComment(repo) {
197357
- const runId = process.env.GITHUB_RUN_ID;
197358
- if (!runId) return "**Run failed.**";
197359
- const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
197360
- const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
197361
- return `**Run failed.** [View the logs \u2192](${url4})`;
197362
- }
197363
- var PROVIDER_BILLING_URLS = Object.fromEntries(
197364
- Object.entries(PROVIDER_DASHBOARDS).map(([id, dashboard]) => [id, dashboard.billing])
197365
- );
197353
+ var RUN_TIMEOUT_PATTERN = /^agent run timed out after (\S+)/;
197354
+ var STOP_SCRIPT_PATTERN = /^stop hook failed \(exit code (-?\d+)\): ([\s\S]*)$/;
197355
+ var UNSUBMITTED_REVIEW_PATTERN = /^\w+ mode finished without calling /;
197366
197356
  function detectProviderId(message2) {
197367
197357
  const harnessId = extractProviderId(message2);
197368
197358
  if (harnessId) return harnessId;
@@ -197374,292 +197364,242 @@ function detectProviderId(message2) {
197374
197364
  if (/platform\.openai\.com/i.test(message2)) return "openai";
197375
197365
  return null;
197376
197366
  }
197377
- var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
197378
- function formatProviderBillingExhausted(input) {
197379
- const providerId = detectProviderId(input.errorMessage);
197380
- const dashboardUrl = providerId ? PROVIDER_BILLING_URLS[providerId] : void 0;
197381
- if (isOpenRouterKeyLimitExceeded(input.errorMessage)) {
197382
- return [
197383
- "**Your OpenRouter key's total limit is too low for this request.**",
197384
- "",
197385
- "OpenRouter refused the request against the key's own spending ceiling, not your account balance \u2014 raising or removing that limit is the fix, and topping up credits alone may not be.",
197386
- "",
197387
- `[Raise the key's limit \u2192](${PROVIDER_DASHBOARDS.openrouter.keys}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
197388
- "",
197389
- `\`\`\`
197390
- ${input.errorMessage}
197391
- \`\`\``
197392
- ].join("\n");
197393
- }
197394
- const spendCap = findAnthropicSpendCap(input.errorMessage);
197395
- if (spendCap) {
197396
- const regains = spendCap.regainAt ? ` Access returns on **${spendCap.regainAt}**.` : "";
197397
- return [
197398
- `**Your \`anthropic\` account has reached its configured API usage limit.**${regains}`,
197399
- "",
197400
- "Anthropic refused the request against a spend limit set in your Console, not an empty balance \u2014 raise or remove that limit to run before it resets.",
197401
- "",
197402
- `[Anthropic billing settings \u2192](${PROVIDER_BILLING_URLS.anthropic})`,
197403
- "",
197404
- `\`\`\`
197405
- ${input.errorMessage}
197406
- \`\`\``
197407
- ].join("\n");
197367
+ function resolveAccount(input) {
197368
+ const subscription = input.credential === "subscription" ? subscriptionFor(input.model) : void 0;
197369
+ if (subscription) {
197370
+ const plan = Object.values(PROVIDER_DASHBOARDS).find(
197371
+ (dashboard2) => dashboard2.credential === subscription.env
197372
+ );
197373
+ return {
197374
+ name: plan?.name ?? null,
197375
+ phrase: subscription.label,
197376
+ billingUrl: plan?.billing,
197377
+ subscription: true
197378
+ };
197408
197379
  }
197409
- const headline = providerId ? `**Your \`${providerId}\` account is out of credit.**` : "**Your provider account is out of credit.**";
197410
- const cta = dashboardUrl ? `[Top up \`${providerId}\` \u2192](${dashboardUrl})` : "Top up your provider account, then re-trigger Pullfrog.";
197411
- return [
197412
- headline,
197413
- "",
197414
- "Pullfrog detected a billing-exhausted response from your provider \u2014 the agent stopped before completing this run.",
197415
- "",
197416
- cta,
197417
- "",
197418
- `\`\`\`
197419
- ${input.errorMessage}
197420
- \`\`\``
197421
- ].join("\n");
197380
+ const id = detectProviderId(input.errorMessage) ?? input.model?.split("/")[0] ?? null;
197381
+ const dashboard = id ? PROVIDER_DASHBOARDS[id] : void 0;
197382
+ const name = dashboard?.name ?? providerDisplayName(id) ?? null;
197383
+ return {
197384
+ name,
197385
+ phrase: name ? `${name} account` : "model provider account",
197386
+ billingUrl: dashboard?.billing,
197387
+ subscription: false
197388
+ };
197422
197389
  }
197423
- function formatTransientUpstreamBody(errorMessage) {
197424
- const providerId = detectProviderId(errorMessage);
197425
- const who = providerId ? `\`${providerId}\`` : "the model provider";
197426
- return [
197427
- `**${who} had a temporary upstream failure.**`,
197428
- "",
197429
- "The provider refused or dropped the request mid-run \u2014 nothing in your repo or your credentials is at fault. Re-trigger Pullfrog; if it keeps happening, check the provider's status page.",
197430
- "",
197431
- `\`\`\`
197432
- ${errorMessage}
197433
- \`\`\``
197434
- ].join("\n");
197390
+ function providerResponse(ctx) {
197391
+ return {
197392
+ summary: ctx.account.name ? `${ctx.account.name}'s response` : "The provider's response",
197393
+ text: ctx.errorMessage
197394
+ };
197435
197395
  }
197436
- function formatProviderNoRoutableEndpoints(input) {
197437
- return [
197438
- "**OpenRouter can't route this model anywhere your account allows.**",
197439
- "",
197440
- "Your OpenRouter data policy excludes every provider currently serving the configured model, so the request was refused before any model work happened. Allow more providers, or pick a model your policy already covers.",
197441
- "",
197442
- `[OpenRouter data policy \u2192](${OPENROUTER_PRIVACY_URL}) \xB7 [Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
197443
- "",
197444
- `\`\`\`
197445
- ${input.errorMessage}
197446
- \`\`\``
197447
- ].join("\n");
197396
+ function formatProviderBillingExhausted(ctx) {
197397
+ if (isOpenRouterKeyLimitExceeded(ctx.errorMessage)) {
197398
+ const openrouter = PROVIDER_DASHBOARDS.openrouter;
197399
+ return renderFailure({
197400
+ headline: "Your OpenRouter key's spending limit is too low for this request.",
197401
+ detail: "OpenRouter checks the key's own limit as well as your balance, so raise or remove the limit; adding credit alone may not be enough.",
197402
+ details: providerResponse(ctx),
197403
+ links: openrouter?.keys ? [
197404
+ { label: "Raise the key's limit", url: openrouter.keys },
197405
+ { label: "Add credit", url: openrouter.billing }
197406
+ ] : []
197407
+ });
197408
+ }
197409
+ const spendCap = findAnthropicSpendCap(ctx.errorMessage);
197410
+ if (spendCap) {
197411
+ const regains = spendCap.regainAt ? `Access returns on **${spendCap.regainAt}**. ` : "";
197412
+ const anthropic = PROVIDER_DASHBOARDS.anthropic;
197413
+ return renderFailure({
197414
+ headline: "Your Anthropic account has reached its API spend limit.",
197415
+ detail: `${regains}This is a limit set in your Anthropic Console, not an empty balance; raise or remove it to run before then.`,
197416
+ details: providerResponse(ctx),
197417
+ links: anthropic ? [{ label: "Anthropic billing settings", url: anthropic.billing }] : []
197418
+ });
197419
+ }
197420
+ return renderFailure({
197421
+ headline: `Your ${ctx.account.phrase} is out of credit.`,
197422
+ detail: "Top it up, then re-trigger Pullfrog.",
197423
+ details: providerResponse(ctx),
197424
+ links: ctx.account.billingUrl ? [{ label: `Top up ${ctx.account.name}`, url: ctx.account.billingUrl }] : []
197425
+ });
197426
+ }
197427
+ function formatProviderUsageLimit(ctx) {
197428
+ const tryAgainAt = /try again at (\d{1,2}:\d{2}\s*[AP]M)/i.exec(ctx.errorMessage)?.[1];
197429
+ const resets = tryAgainAt ? `${ctx.account.name ?? "The provider"} says to try again at ${tryAgainAt}. ` : "";
197430
+ return renderFailure({
197431
+ headline: `Your ${ctx.account.phrase} has hit its usage limit.`,
197432
+ detail: ctx.account.subscription ? `${resets}It resets on its own, so re-trigger after that, or connect an API key, which Pullfrog falls back to automatically.` : `${resets}Raise the limit with the provider, or re-trigger once it resets.`,
197433
+ details: providerResponse(ctx),
197434
+ links: [providerLink(ctx.repo.owner), pickModelLink(ctx.repo)]
197435
+ });
197436
+ }
197437
+ function formatTransientUpstreamBody(ctx) {
197438
+ if (/\bmodel is at capacity\b/i.test(ctx.errorMessage)) {
197439
+ return renderFailure({
197440
+ headline: `${ctx.account.name ? `${ctx.account.name}'s model` : "The model"} is at capacity right now.`,
197441
+ detail: "Re-trigger in a few minutes, or pick another model.",
197442
+ details: providerResponse(ctx),
197443
+ links: [pickModelLink(ctx.repo)]
197444
+ });
197445
+ }
197446
+ return renderFailure({
197447
+ headline: `${ctx.account.name ?? "The model provider"} had a temporary failure.`,
197448
+ detail: "Nothing on your side is at fault. Re-trigger to try again; if it keeps happening, check the provider's status page.",
197449
+ details: providerResponse(ctx)
197450
+ });
197451
+ }
197452
+ function formatProviderNoRoutableEndpoints(ctx) {
197453
+ return renderFailure({
197454
+ headline: "OpenRouter can't route this model anywhere your account allows.",
197455
+ detail: "Your OpenRouter data policy excludes every provider serving it, so nothing ran and nothing was billed. Allow more providers, or pick a model your policy covers.",
197456
+ details: providerResponse(ctx),
197457
+ links: [
197458
+ { label: "OpenRouter data policy", url: "https://openrouter.ai/settings/privacy" },
197459
+ pickModelLink(ctx.repo)
197460
+ ]
197461
+ });
197448
197462
  }
197449
- function formatProviderMissingCredential(input) {
197450
- return [
197451
- "**No credential reached the model provider.**",
197452
- "",
197453
- "The run started without a key for the provider it ended up using, so the request was refused before any model work happened. Nothing was billed, and no key of yours was rejected \u2014 add a provider key, or pick a model you already have one for.",
197454
- "",
197455
- `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
197456
- "",
197457
- `\`\`\`
197458
- ${input.errorMessage}
197459
- \`\`\``
197460
- ].join("\n");
197463
+ function formatProviderMissingCredential(ctx) {
197464
+ return renderFailure({
197465
+ headline: "No credential reached the model provider.",
197466
+ detail: "The run used a provider it had no key for, so nothing was billed and no key of yours was rejected. Connect a key for that provider, or pick a model you have one for.",
197467
+ details: providerResponse(ctx),
197468
+ links: [providerLink(ctx.repo.owner), pickModelLink(ctx.repo)]
197469
+ });
197461
197470
  }
197462
- function formatByokSetupSummary(input) {
197463
- return [
197464
- "**This repo's model isn't fully set up yet.**",
197465
- "",
197466
- input.raw,
197467
- "",
197468
- `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`
197469
- ].join("\n");
197471
+ function formatProviderModelNotFound(ctx) {
197472
+ const model = /Model not found: (\S+?)\.?(?:\s|$)/.exec(ctx.errorMessage)?.[1];
197473
+ const suggestions = /Did you mean: ([^?\n]+)\?/.exec(ctx.errorMessage)?.[1]?.split(",").map((id) => `\`${id.trim()}\``);
197474
+ const suggested = suggestions?.length ? ` OpenCode suggests ${suggestions.slice(0, -1).join(", ")}${suggestions.length > 1 ? " or " : ""}${suggestions.at(-1)}.` : "";
197475
+ return renderFailure({
197476
+ headline: model ? `\`${model}\` isn't in OpenCode's catalog.` : "This repo's model isn't in OpenCode's catalog.",
197477
+ detail: `Pick another model wherever this repo sets it: the Pullfrog console, the workflow's \`model\` input or the \`PULLFROG_MODEL\` variable.${suggested}`,
197478
+ details: { summary: "OpenCode's response", text: ctx.errorMessage },
197479
+ links: [pickModelLink(ctx.repo)]
197480
+ });
197470
197481
  }
197471
- function formatProviderModelNotFoundSummary(input) {
197472
- return `The configured model is not in OpenCode's catalog. Pick a different model for \`${input.owner}/${input.name}\` where it is set \u2014 the Pullfrog console, the workflow's \`model\` input, or the \`PULLFROG_MODEL\` variable \u2014 or contact support if this persists.
197473
-
197474
- \`\`\`
197475
- ${input.raw}
197476
- \`\`\``;
197482
+ function formatNoProviderAvailable(ctx) {
197483
+ return renderFailure({
197484
+ headline: "The provider won't serve this model to this account.",
197485
+ detail: "It's in the catalog, but this account's plan can't reach it, which often happens when Pullfrog auto-selects it. Pin a model for this repo, or connect the provider that was picked.",
197486
+ details: providerResponse(ctx),
197487
+ links: [pickModelLink(ctx.repo), providerLink(ctx.repo.owner)]
197488
+ });
197477
197489
  }
197478
- function formatProviderUsageLimit(input) {
197479
- return [
197480
- "**The provider's usage limit for this account has been reached.**",
197481
- "",
197482
- "A subscription window resets on its own, so re-trigger after it does; a limit set on a pay-as-you-go workspace has to be raised. A model on another provider can run in the meantime.",
197483
- "",
197484
- `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
197485
- "",
197486
- `\`\`\`
197487
- ${input.raw}
197488
- \`\`\``
197489
- ].join("\n");
197490
+ function formatZenFreeTierGate(ctx) {
197491
+ return renderFailure({
197492
+ headline: "OpenCode Zen's free tier refused a request from this run.",
197493
+ detail: "Zen rejects free-model requests that don't list its built-in tools, and OpenCode's mid-run context compaction sends none ([upstream issue](https://github.com/anomalyco/opencode/issues/49587)). Pick a paid model, or split the PR so the run finishes before compaction. If this repo's `opencode.json` denies `bash`, `glob`, `grep` or `read`, remove that too.",
197494
+ details: { summary: "OpenCode Zen's response", text: ctx.errorMessage },
197495
+ links: [pickModelLink(ctx.repo)]
197496
+ });
197490
197497
  }
197491
- function formatProviderErrorBody(input) {
197492
- return [
197493
- "**The model provider returned an error, so this run stopped.**",
197494
- "",
197495
- "The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
197496
- "",
197497
- `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
197498
- "",
197499
- `\`\`\`
197500
- ${input.raw}
197501
- \`\`\``
197502
- ].join("\n");
197498
+ function formatUnrecognized(ctx) {
197499
+ return renderFailure({
197500
+ headline: "An unrecognized error occurred.",
197501
+ detail: "The error is below.",
197502
+ error: ctx.errorMessage,
197503
+ links: [DISCORD_LINK]
197504
+ });
197503
197505
  }
197504
- function renderRunError(input) {
197505
- const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
197506
- if (billingError) {
197507
- const body = formatBillingErrorSummary(billingError, input.repo.owner);
197508
- return { summary: body, comment: body };
197509
- }
197510
- if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
197511
- return { summary: input.errorMessage, comment: input.errorMessage };
197512
- }
197513
- if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
197514
- return { summary: input.errorMessage, comment: input.errorMessage };
197506
+ function renderRunErrorBody(ctx) {
197507
+ const message2 = ctx.errorMessage;
197508
+ if (ctx.trial && isRouterKeylimitExhaustedError(message2)) {
197509
+ return buildTrialSpentError({ owner: ctx.repo.owner });
197515
197510
  }
197516
- if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
197517
- return { summary: input.errorMessage, comment: input.errorMessage };
197518
- }
197519
- if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
197520
- return { summary: input.errorMessage, comment: input.errorMessage };
197521
- }
197522
- const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
197523
- const hangBody = isHang ? formatAgentHangBody({
197524
- diagnostic: input.agentDiagnostic,
197525
- isHang: true,
197526
- errorMessage: input.errorMessage
197527
- }) : null;
197528
- if (isProviderBillingExhausted(input.errorMessage)) {
197529
- const body = formatProviderBillingExhausted({ errorMessage: input.errorMessage });
197530
- return { summary: `### \u274C Pullfrog failed
197531
-
197532
- ${body}`, comment: body };
197533
- }
197534
- if (isProviderNoRoutableEndpoints(input.errorMessage)) {
197535
- const body = formatProviderNoRoutableEndpoints({
197536
- owner: input.repo.owner,
197537
- name: input.repo.name,
197538
- errorMessage: input.errorMessage
197539
- });
197540
- return { summary: `### \u274C Pullfrog failed
197541
-
197542
- ${body}`, comment: body };
197543
- }
197544
- if (isProviderUsageLimit(input.errorMessage)) {
197545
- const body = formatProviderUsageLimit({
197546
- owner: input.repo.owner,
197547
- name: input.repo.name,
197548
- raw: input.errorMessage
197549
- });
197550
- return { summary: `### \u274C Pullfrog failed
197551
-
197552
- ${body}`, comment: body };
197553
- }
197554
- if (isProviderMissingCredential(input.errorMessage)) {
197555
- const body = formatProviderMissingCredential({
197556
- owner: input.repo.owner,
197557
- name: input.repo.name,
197558
- errorMessage: input.errorMessage
197559
- });
197560
- return { summary: `### \u274C Pullfrog failed
197561
-
197562
- ${body}`, comment: body };
197563
- }
197564
- if (isByokSetupError(input.errorMessage)) {
197565
- const body = formatByokSetupSummary({
197566
- owner: input.repo.owner,
197567
- name: input.repo.name,
197568
- raw: input.errorMessage
197569
- });
197570
- return { summary: `### \u274C Pullfrog failed
197571
-
197572
- ${body}`, comment: body };
197573
- }
197574
- const apiKeySource = hangBody ?? input.errorMessage;
197575
- const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
197576
- owner: input.repo.owner,
197577
- name: input.repo.name,
197578
- raw: apiKeySource
197579
- }) : null;
197580
- if (apiKeyErrorSummary) {
197581
- return { summary: apiKeyErrorSummary, comment: apiKeyErrorSummary };
197582
- }
197583
- if (isProviderModelNotFoundError(input.errorMessage)) {
197584
- const body = formatProviderModelNotFoundSummary({
197585
- owner: input.repo.owner,
197586
- name: input.repo.name,
197587
- raw: input.errorMessage
197588
- });
197589
- return { summary: body, comment: body };
197511
+ if (ctx.routerActive && isRouterKeylimitExhaustedError(message2)) {
197512
+ return formatBillingErrorSummary(
197513
+ new BillingError(message2, { code: "router_keylimit_exhausted" }),
197514
+ ctx.repo.owner
197515
+ );
197590
197516
  }
197591
- if (isNoProviderAvailableError(input.errorMessage)) {
197592
- const body = formatNoProviderAvailableSummary({
197593
- owner: input.repo.owner,
197594
- name: input.repo.name,
197595
- raw: input.errorMessage
197517
+ if ([
197518
+ MODEL_ACCESS_MARKER,
197519
+ SECRETS_UNAVAILABLE_MARKER,
197520
+ ROUTER_UNFUNDED_MARKER,
197521
+ TRIAL_SPENT_MARKER,
197522
+ CREDENTIAL_REJECTED_MARKER,
197523
+ CREDENTIAL_POOL_REFUSED_MARKER
197524
+ ].some((marker) => message2.includes(marker))) {
197525
+ return message2;
197526
+ }
197527
+ const isHang = message2.startsWith("activity timeout") || message2.startsWith("agent still pending");
197528
+ const hangBody = isHang ? formatAgentHangBody({ diagnostic: ctx.agentDiagnostic, errorMessage: message2 }) : null;
197529
+ if (isProviderBillingExhausted(message2)) return formatProviderBillingExhausted(ctx);
197530
+ if (isProviderNoRoutableEndpoints(message2)) return formatProviderNoRoutableEndpoints(ctx);
197531
+ if (isProviderUsageLimit(message2)) return formatProviderUsageLimit(ctx);
197532
+ if (isProviderMissingCredential(message2)) return formatProviderMissingCredential(ctx);
197533
+ if (isByokSetupError(message2)) {
197534
+ return renderFailure({
197535
+ headline: "This repo's model isn't fully set up yet.",
197536
+ detail: message2
197537
+ });
197538
+ }
197539
+ const apiKeySource = hangBody ?? message2;
197540
+ if (isApiKeyAuthError(apiKeySource)) {
197541
+ return formatApiKeyErrorSummary({
197542
+ owner: ctx.repo.owner,
197543
+ name: ctx.repo.name,
197544
+ raw: apiKeySource,
197545
+ model: ctx.model,
197546
+ credential: ctx.credential,
197547
+ output: hangBody ? void 0 : message2
197548
+ });
197549
+ }
197550
+ if (isProviderModelNotFoundError(message2)) return formatProviderModelNotFound(ctx);
197551
+ if (isNoProviderAvailableError(message2)) return formatNoProviderAvailable(ctx);
197552
+ if (isZenFreeTierGateError(message2)) return formatZenFreeTierGate(ctx);
197553
+ if (isContextOverflowError(message2)) {
197554
+ return renderFailure({
197555
+ headline: "This run outgrew the model's context window.",
197556
+ detail: "Pick a model with a larger context window, or split the PR into smaller ones and re-trigger.",
197557
+ links: [pickModelLink(ctx.repo)]
197558
+ });
197559
+ }
197560
+ const timeout = RUN_TIMEOUT_PATTERN.exec(message2)?.[1];
197561
+ if (timeout) {
197562
+ return renderFailure({
197563
+ headline: `This run hit its ${timeout} time limit while still working.`,
197564
+ detail: "Give runs longer with `--timeout=2h` (or `--notimeout`) in this repo's instructions, or raise the workflow's `timeout` input if it sets one, since that input wins. A smaller PR or a lower effort also finishes sooner.",
197565
+ links: [
197566
+ { label: "Repo settings", url: repoConsoleUrl(ctx.repo) },
197567
+ { label: "Flags", url: "https://docs.pullfrog.com/flags" }
197568
+ ]
197596
197569
  });
197597
- return { summary: `### \u274C Pullfrog failed
197598
-
197599
- ${body}`, comment: body };
197600
197570
  }
197601
- if (isZenFreeTierGateError(input.errorMessage)) {
197602
- const body = formatZenFreeTierGateSummary({
197603
- owner: input.repo.owner,
197604
- name: input.repo.name,
197605
- raw: input.errorMessage
197571
+ const stopScript = STOP_SCRIPT_PATTERN.exec(message2);
197572
+ if (stopScript) {
197573
+ return renderFailure({
197574
+ headline: "This repo's stop script failed, so the run ended without finishing.",
197575
+ detail: `It exited with code ${stopScript[1]}. Fix the script in this repo's settings, or re-trigger if the failure was temporary.`,
197576
+ details: stopScript[2]?.trim() ? { summary: "Script output", text: stopScript[2].trim() } : void 0,
197577
+ links: [{ label: "Repo settings", url: repoConsoleUrl(ctx.repo) }]
197606
197578
  });
197607
- return { summary: `### \u274C Pullfrog failed
197608
-
197609
- ${body}`, comment: body };
197610
197579
  }
197611
- if (isContextOverflowError(input.errorMessage)) {
197612
- const body = formatContextOverflowSummary({
197613
- owner: input.repo.owner,
197614
- name: input.repo.name,
197615
- raw: input.errorMessage
197580
+ if (UNSUBMITTED_REVIEW_PATTERN.test(message2)) {
197581
+ return renderFailure({
197582
+ headline: "The agent finished without posting its review.",
197583
+ detail: "It was reminded and still didn't, so the run stopped. Re-trigger to try again."
197616
197584
  });
197617
- return { summary: `### \u274C Pullfrog failed
197618
-
197619
- ${body}`, comment: body };
197620
197585
  }
197621
- if (isRunTimeoutError(input.errorMessage)) {
197622
- const body = formatRunTimeoutSummary({
197623
- owner: input.repo.owner,
197624
- name: input.repo.name,
197625
- raw: input.errorMessage
197586
+ if (isTransientUpstreamError(message2)) return formatTransientUpstreamBody(ctx);
197587
+ if (hangBody) return hangBody;
197588
+ if (message2.startsWith("provider error: ")) {
197589
+ return renderFailure({
197590
+ headline: `${ctx.account.name ?? "The model provider"} returned an error, so this run stopped.`,
197591
+ detail: "If it names your key, plan or balance, fix that with the provider or pick another model.",
197592
+ error: message2.slice("provider error: ".length),
197593
+ links: [pickModelLink(ctx.repo)]
197626
197594
  });
197627
- return { summary: `### \u274C Pullfrog failed
197628
-
197629
- ${body}`, comment: body };
197630
- }
197631
- if (isTransientUpstreamError(input.errorMessage)) {
197632
- const body = formatTransientUpstreamBody(input.errorMessage);
197633
- return { summary: `### \u274C Pullfrog failed
197634
-
197635
- ${body}`, comment: body };
197636
- }
197637
- if (hangBody) {
197638
- const explained = input.agentDiagnostic?.lastProviderError !== void 0 || input.agentDiagnostic?.sawModelOutput === false;
197639
- return {
197640
- summary: `### \u274C Pullfrog failed
197641
-
197642
- ${hangBody}`,
197643
- comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
197644
- };
197645
197595
  }
197646
- if (input.errorMessage.startsWith("provider error: ")) {
197647
- const body = formatProviderErrorBody({
197648
- owner: input.repo.owner,
197649
- name: input.repo.name,
197650
- raw: input.errorMessage
197651
- });
197652
- return { summary: `### \u274C Pullfrog failed
197596
+ return formatUnrecognized(ctx);
197597
+ }
197598
+ function renderRunError(input) {
197599
+ const body = renderRunErrorBody({ ...input, account: resolveAccount(input) });
197600
+ return { summary: `### \u274C Pullfrog failed
197653
197601
 
197654
197602
  ${body}`, comment: body };
197655
- }
197656
- const genericBody = formatGenericFailure(input.errorMessage);
197657
- return {
197658
- summary: `### \u274C Pullfrog failed
197659
-
197660
- ${genericBody}`,
197661
- comment: formatMinimalFailureComment(input.repo)
197662
- };
197663
197603
  }
197664
197604
 
197665
197605
  // utils/runLifecycle.ts
@@ -198085,7 +198025,10 @@ async function finalizeSuccessRun(input) {
198085
198025
  errorMessage: input.result.error || "agent run failed",
198086
198026
  repo: input.repo,
198087
198027
  agentDiagnostic: input.toolState.agentDiagnostic,
198088
- routerActive: !!input.toolContext.payload.proxyModel
198028
+ routerActive: !!input.toolContext.payload.proxyModel,
198029
+ model: input.toolState.model,
198030
+ credential: input.toolState.credential,
198031
+ trial: input.toolState.modelClamped?.reason === "trial"
198089
198032
  }) : null;
198090
198033
  if (rendered) {
198091
198034
  await reportErrorToComment({
@@ -198246,7 +198189,7 @@ function renderTodoMarkdown(todos) {
198246
198189
  case "cancelled":
198247
198190
  return `- ~~${todo.content}~~`;
198248
198191
  case "in_progress":
198249
- return `- [ ] <img src="https://uploads.pullfrog.com/Progress%20Indicator.gif" width="11" style="visibility: visible; max-width: 100%;" /> ${todo.content}`;
198192
+ return `- [ ] <img src="${TODO_SPINNER_URL}" width="11" style="visibility: visible; max-width: 100%;" /> ${todo.content}`;
198250
198193
  case "pending":
198251
198194
  return `- [ ] ${todo.content}`;
198252
198195
  default:
@@ -198633,10 +198576,15 @@ async function main() {
198633
198576
  const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
198634
198577
  payload,
198635
198578
  configuredModel: effectiveModel,
198579
+ creditsLeftUsd: runContext.trialCreditsLeftUsd,
198636
198580
  oidcCredentials,
198637
198581
  repo: runContext.repo,
198638
198582
  toolState
198639
198583
  }) : false;
198584
+ const trialSpent = runContext.trialCreditsLeftUsd !== void 0 && runContext.trialCreditsLeftUsd <= 0;
198585
+ if (!funded && trialSpent && missingKey instanceof NoUsableCredentialError) {
198586
+ throw new Error(buildTrialSpentError({ owner: runContext.repo.owner }));
198587
+ }
198640
198588
  if (!funded) throw missingKey;
198641
198589
  agent2 = resolveAgent2({
198642
198590
  model: resolvedModel,
@@ -198967,7 +198915,10 @@ ${instructions.user}` : null,
198967
198915
  errorMessage,
198968
198916
  repo: runContext.repo,
198969
198917
  agentDiagnostic: toolState.agentDiagnostic,
198970
- routerActive: !!payload.proxyModel
198918
+ routerActive: !!payload.proxyModel,
198919
+ model: toolState.model,
198920
+ credential: toolState.credential,
198921
+ trial: toolState.modelClamped?.reason === "trial"
198971
198922
  });
198972
198923
  await writeRunErrorOutputs({ rendered, toolState });
198973
198924
  if (toolContext) {