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/cli.mjs CHANGED
@@ -64372,7 +64372,7 @@ var require_receiver2 = __commonJS({
64372
64372
  const buffer = this.consume(2);
64373
64373
  const fin = (buffer[0] & 128) !== 0;
64374
64374
  const opcode = buffer[0] & 15;
64375
- const masked = (buffer[1] & 128) === 128;
64375
+ const masked2 = (buffer[1] & 128) === 128;
64376
64376
  const fragmented = !fin && opcode !== opcodes.CONTINUATION;
64377
64377
  const payloadLength = buffer[1] & 127;
64378
64378
  const rsv1 = buffer[0] & 64;
@@ -64382,7 +64382,7 @@ var require_receiver2 = __commonJS({
64382
64382
  failWebsocketConnection(this.#handler, 1002, "Invalid opcode received");
64383
64383
  return callback();
64384
64384
  }
64385
- if (masked) {
64385
+ if (masked2) {
64386
64386
  failWebsocketConnection(this.#handler, 1002, "Frame cannot be masked");
64387
64387
  return callback();
64388
64388
  }
@@ -64430,7 +64430,7 @@ var require_receiver2 = __commonJS({
64430
64430
  this.#info.compressed = rsv1 !== 0;
64431
64431
  }
64432
64432
  this.#info.opcode = opcode;
64433
- this.#info.masked = masked;
64433
+ this.#info.masked = masked2;
64434
64434
  this.#info.fin = fin;
64435
64435
  this.#info.fragmented = fragmented;
64436
64436
  } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
@@ -107049,7 +107049,7 @@ var import_picocolors = __toESM(require_picocolors(), 1);
107049
107049
  // package.json
107050
107050
  var package_default = {
107051
107051
  name: "pullfrog",
107052
- version: "0.1.100",
107052
+ version: "0.1.101",
107053
107053
  cliContractVersion: "1",
107054
107054
  type: "module",
107055
107055
  bin: {
@@ -110574,6 +110574,13 @@ if (!defaultProxyAlias?.openRouterResolve) {
110574
110574
  }
110575
110575
  var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
110576
110576
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
110577
+ var trialProxyAlias = resolveDisplayAlias("openai/gpt-luna");
110578
+ if (!trialProxyAlias?.openRouterResolve) {
110579
+ throw new Error("TRIAL_PROXY_MODEL: openai/gpt-luna has no openRouterResolve");
110580
+ }
110581
+ var TRIAL_PROXY_MODEL = trialProxyAlias.openRouterResolve;
110582
+ var TRIAL_PROXY_DISPLAY_NAME = trialProxyAlias.displayName;
110583
+ var TRIAL_FUNDED_USD_LIMIT = 10;
110577
110584
  for (const [name, ladder] of Object.entries({ ...PROVIDER_LADDERS, router: ROUTER_LADDER })) {
110578
110585
  const terminal = /* @__PURE__ */ new Set();
110579
110586
  for (const slug2 of Object.values(ladder)) {
@@ -111281,6 +111288,8 @@ var PROVIDER_ERROR_PATTERNS = [
111281
111288
  },
111282
111289
  // OpenAI's `You have no credits remaining` (#1474).
111283
111290
  { regex: /no credits remaining/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111291
+ // Codex on a ChatGPT workspace: `Your workspace is out of credits. Add credits to continue.`
111292
+ { regex: /workspace is out of credits/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111284
111293
  { regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
111285
111294
  // "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
111286
111295
  // condition in two house styles — a configured ceiling was reached.
@@ -111310,6 +111319,9 @@ var PROVIDER_ERROR_PATTERNS = [
111310
111319
  // succeeds" while the same log held nine of these. Measured on a live account
111311
111320
  // whose ChatGPT subscription was capped.
111312
111321
  { regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
111322
+ // the Codex CLI's wording for the same capped plan: `You’ve hit your usage limit. … try
111323
+ // again at 7:58 AM.` Claude's `hit your session limit` names its window and stays out.
111324
+ { regex: /\bhit your usage limit\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
111313
111325
  // the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
111314
111326
  // run those mean the PULLFROG wallet is empty and `renderRunError` returns a
111315
111327
  // `BillingError` before ever reaching this list; on a BYOK run the very same
@@ -111414,7 +111426,8 @@ function isProviderMissingCredential(text) {
111414
111426
  return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
111415
111427
  }
111416
111428
  function isTransientUpstreamError(text) {
111417
- 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);
111429
+ 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.`
111430
+ /\bmodel is at capacity\b/i.test(text);
111418
111431
  }
111419
111432
  function extractProviderId(text) {
111420
111433
  const match3 = text.match(/\bproviderID=([a-z0-9_-]+)/i);
@@ -113799,9 +113812,20 @@ async function writeBackEntry(apiToken, entry) {
113799
113812
 
113800
113813
  // utils/secretCommands.ts
113801
113814
  var core3 = __toESM(require_core(), 1);
113815
+ var masked = /* @__PURE__ */ new Set();
113802
113816
  function maskSecret(value2) {
113817
+ masked.add(value2);
113803
113818
  if (isGitHubActions) core3.setSecret(value2);
113804
113819
  }
113820
+ function redactSecrets(text) {
113821
+ let redacted = text;
113822
+ for (const value2 of masked) {
113823
+ for (const part of [value2, ...value2.split("\n").map((line) => line.trim())]) {
113824
+ if (part.length >= 8) redacted = redacted.replaceAll(part, "***");
113825
+ }
113826
+ }
113827
+ return redacted;
113828
+ }
113805
113829
  function saveSecretState(name, value2) {
113806
113830
  if (isGitHubActions) core3.saveState(name, value2);
113807
113831
  }
@@ -192602,7 +192626,7 @@ function formatModelLabel(params) {
192602
192626
  }
192603
192627
  const base = alias?.isFree ? `\`${modelName}\` (free)` : `\`${modelName}\``;
192604
192628
  if (params.clamped?.reason === "trial") {
192605
- return `${base} (model usage covered by Pullfrog)`;
192629
+ return `${base} (free trial credits)`;
192606
192630
  }
192607
192631
  if (params.clamped) {
192608
192632
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
@@ -192613,13 +192637,14 @@ function formatModelLabel(params) {
192613
192637
  }
192614
192638
  return base;
192615
192639
  }
192616
- function buildTrialDisclosure(owner) {
192617
- const consoleUrl2 = owner ? `https://pullfrog.com/console/${owner}` : "https://pullfrog.com/console";
192640
+ function buildTrialDisclosure(params) {
192641
+ const consoleUrl2 = params.owner ? `https://pullfrog.com/console/${params.owner}#billing` : "https://pullfrog.com/console";
192642
+ const left = params.creditsLeftUsd === void 0 ? "" : ` ($${Math.max(0, params.creditsLeftUsd).toFixed(2)} of $${TRIAL_FUNDED_USD_LIMIT} left)`;
192618
192643
  return [
192619
192644
  "---",
192620
192645
  "",
192621
192646
  "> [!IMPORTANT]",
192622
- `> **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](${consoleUrl2})`
192647
+ `> **This run used your free trial credits**${left} on \`${TRIAL_PROXY_DISPLAY_NAME}\`, a fast, lightweight model. [Connect a provider \u2192](${consoleUrl2}) to run the model you choose.`
192623
192648
  ].join("\n");
192624
192649
  }
192625
192650
  function buildPullfrogFooter(params) {
@@ -192642,13 +192667,13 @@ function buildPullfrogFooter(params) {
192642
192667
  `Using ${formatModelLabel({
192643
192668
  model: params.model,
192644
192669
  clamped: params.clamped,
192645
- unselectedProxyDefault: params.unselectedProxyDefault,
192670
+ unselectedProxyDefault: params.unselectedProxyDefault && !params.failed,
192646
192671
  oss: params.oss
192647
192672
  })}`
192648
192673
  );
192649
192674
  }
192650
- const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
192651
- const disclosure = params.clamped?.reason === "trial" ? `${buildTrialDisclosure(params.owner)}
192675
+ const allParts = params.failed ? parts : [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
192676
+ const disclosure = params.clamped?.reason === "trial" && !params.failed ? `${buildTrialDisclosure({ owner: params.owner, creditsLeftUsd: params.clamped.creditsLeftUsd })}
192652
192677
 
192653
192678
  ` : "";
192654
192679
  return `
@@ -192826,7 +192851,8 @@ async function fetchRunContext(params) {
192826
192851
  credentialAccess: data.credentialAccess,
192827
192852
  secretsUnavailable: data.secretsUnavailable,
192828
192853
  routerUnfunded: data.routerUnfunded,
192829
- trialFallback: data.trialFallback
192854
+ trialFallback: data.trialFallback,
192855
+ trialCreditsLeftUsd: data.trialCreditsLeftUsd
192830
192856
  };
192831
192857
  } catch {
192832
192858
  clearTimeout(timeoutId);
@@ -192940,6 +192966,9 @@ function aggregateUsage(entries) {
192940
192966
  return out;
192941
192967
  }
192942
192968
 
192969
+ // utils/leapingComment.ts
192970
+ var TODO_SPINNER_URL = "https://uploads.pullfrog.com/Progress%20Indicator.gif";
192971
+
192943
192972
  // mcp/comment.ts
192944
192973
  function isNotFoundError(error49) {
192945
192974
  return error49 instanceof Error && error49.message.includes("Not Found");
@@ -195051,7 +195080,7 @@ function mirrorRolePermissions(role) {
195051
195080
  case "maintain":
195052
195081
  case "write":
195053
195082
  case "triage":
195054
- return { contents: "read", issues: "write", pull_requests: "write" };
195083
+ return { checks: "read", contents: "read", issues: "write", pull_requests: "write" };
195055
195084
  case "read":
195056
195085
  case "none":
195057
195086
  case void 0:
@@ -197720,6 +197749,40 @@ function subagentDeniedToolNames(ctx, outputSchema) {
197720
197749
  return names;
197721
197750
  }
197722
197751
 
197752
+ // utils/failureBody.ts
197753
+ function renderFailure(params) {
197754
+ const blocks = [
197755
+ params.detail ? `**${params.headline}** ${params.detail}` : `**${params.headline}**`
197756
+ ];
197757
+ if (params.list) blocks.push(params.list.map((item) => `- ${item}`).join("\n"));
197758
+ if (params.error) blocks.push(fenced(params.error));
197759
+ if (params.details) {
197760
+ blocks.push(
197761
+ `<details><summary>${params.details.summary}</summary>
197762
+
197763
+ ${fenced(params.details.text)}
197764
+
197765
+ </details>`
197766
+ );
197767
+ }
197768
+ if (params.links?.length) {
197769
+ blocks.push(params.links.map((link2) => `[${link2.label} \u2192](${link2.url})`).join(" \xB7 "));
197770
+ }
197771
+ return blocks.join("\n\n");
197772
+ }
197773
+ function fenced(text) {
197774
+ let longest = 0;
197775
+ for (const match3 of text.matchAll(/`+/g)) longest = Math.max(longest, match3[0].length);
197776
+ const fence = "`".repeat(Math.max(3, longest + 1));
197777
+ return `${fence}
197778
+ ${text}
197779
+ ${fence}`;
197780
+ }
197781
+ var DISCORD_LINK = {
197782
+ label: "Ask in Discord",
197783
+ url: "https://discord.gg/8y96raFg8e"
197784
+ };
197785
+
197723
197786
  // utils/providerDashboards.ts
197724
197787
  var PROVIDER_DASHBOARDS = {
197725
197788
  anthropic: {
@@ -197830,134 +197893,145 @@ var PROVIDER_DASHBOARDS = {
197830
197893
  };
197831
197894
 
197832
197895
  // utils/apiKeys.ts
197833
- var MISSING_KEY_MARKER = "no API key found";
197834
- var BYOK_SLUG_MARKER = "env var is required when the model is set to";
197835
- var BYOK_CONFIG_MARKER = "selected but required configuration is missing:";
197896
+ var MISSING_KEY_MARKER = "No API key found";
197897
+ var BYOK_SLUG_MARKER = "must be set when the model is";
197898
+ var BYOK_CONFIG_MARKER = "is missing required settings:";
197836
197899
  var BYOK_SETUP_PATTERN = new RegExp(
197837
- `^(?:[A-Z][A-Z0-9_]+ ${BYOK_SLUG_MARKER}|[A-Z][\\w-]*(?: [\\w-]+){0,3} ${BYOK_CONFIG_MARKER})`
197900
+ `^(?:\`[A-Z][A-Z0-9_]+\` ${BYOK_SLUG_MARKER}|[A-Z][\\w-]*(?: [\\w-]+){0,3} ${BYOK_CONFIG_MARKER})`
197838
197901
  );
197839
197902
  function isByokSetupError(text) {
197840
197903
  return BYOK_SETUP_PATTERN.test(text);
197841
197904
  }
197842
- var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
197843
- var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
197905
+ var SECRETS_UNAVAILABLE_MARKER = "couldn't load your stored secrets";
197906
+ var ROUTER_UNFUNDED_MARKER = "Your Pullfrog Router balance is empty";
197907
+ var TRIAL_SPENT_MARKER = "reached the limit of your free trial credits";
197908
+ function buildTrialSpentError(params) {
197909
+ return renderFailure({
197910
+ headline: `You've ${TRIAL_SPENT_MARKER}.`,
197911
+ 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.`,
197912
+ links: [providerLink(params.owner)]
197913
+ });
197914
+ }
197915
+ function repoConsoleUrl(repo) {
197916
+ return `${getApiUrl()}/console/${repo.owner}/${repo.name}`;
197917
+ }
197918
+ function pickModelLink(repo) {
197919
+ return { label: "Pick another model", url: repoConsoleUrl(repo) };
197920
+ }
197921
+ function providerLink(owner) {
197922
+ return { label: "Connect a provider", url: `${getApiUrl()}/console/${owner}#billing` };
197923
+ }
197844
197924
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
197845
197925
  var CREDENTIAL_POOL_REFUSED_MARKER = "No configured credential can serve this run";
197846
197926
  var NoUsableCredentialError = class extends Error {
197847
197927
  };
197848
197928
  function throwKeyError(params) {
197849
- if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError(params));
197929
+ if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError());
197850
197930
  if (params.routerUnfunded) throw new NoUsableCredentialError(buildRouterUnfundedError(params));
197851
197931
  throw new NoUsableCredentialError(buildMissingApiKeyError(params));
197852
197932
  }
197853
197933
  function buildRouterUnfundedError(params) {
197854
- const billingUrl = `${getApiUrl()}/console/${params.owner}#billing`;
197855
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
197856
- const modelClause = params.model ? ` \`${params.model}\` never ran.` : " the agent never ran.";
197857
- return [
197858
- `**${ROUTER_UNFUNDED_MARKER}**, and this repo has no provider key to fall back on, so${modelClause}`,
197859
- "",
197860
- "**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.",
197861
- "",
197862
- `[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)`
197863
- ].join("\n");
197934
+ return renderFailure({
197935
+ headline: `${ROUTER_UNFUNDED_MARKER}, and this repo has no API key to fall back on.`,
197936
+ detail: "Add Router credit, or connect your own provider key or subscription.",
197937
+ links: [
197938
+ { label: "Billing and providers", url: `${getApiUrl()}/console/${params.owner}#billing` }
197939
+ ]
197940
+ });
197864
197941
  }
197865
- function buildSecretsUnavailableError(params) {
197866
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
197867
- const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
197868
- return [
197869
- `**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.`,
197870
- "",
197871
- "**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
197872
- "",
197873
- `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
197874
- ].join("\n");
197942
+ function buildSecretsUnavailableError() {
197943
+ return renderFailure({
197944
+ headline: `Pullfrog ${SECRETS_UNAVAILABLE_MARKER} for this run.`,
197945
+ 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.",
197946
+ links: [DISCORD_LINK]
197947
+ });
197875
197948
  }
197876
197949
  function buildMissingApiKeyError(params) {
197877
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197878
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
197879
197950
  const envVars = params.model?.includes("/") ? getModelEnvVars(params.model) : [];
197880
197951
  const [primary, ...alternates] = envVars;
197881
197952
  const envVarList = primary ? `\`${primary}\`${alternates.length > 0 ? ` (or ${alternates.map((v) => `\`${v}\``).join(" / ")})` : ""}` : void 0;
197882
- 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.`;
197953
+ const alias = params.model ? resolveDisplayAlias(params.model) ?? modelAliases.find((a2) => !a2.fallback && a2.resolve === params.model) : void 0;
197954
+ const modelName = params.model ? alias?.displayName ?? `\`${params.model}\`` : void 0;
197883
197955
  const unhosted = params.model?.includes("/") ? getModelManagedCredentials(params.model).find((name) => unhostedSubscriptions.has(name)) : void 0;
197884
- 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).";
197956
+ 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.";
197957
+ return renderFailure({
197958
+ headline: envVarList && modelName ? `${MISSING_KEY_MARKER} for ${modelName}.` : `${MISSING_KEY_MARKER}.`,
197959
+ detail,
197960
+ links: [providerLink(params.owner), pickModelLink(params)]
197961
+ });
197962
+ }
197963
+ function buildSetupError(params) {
197964
+ const missing = params.missing.map((name) => `\`${name}\``).join(", ");
197885
197965
  return [
197886
- lead,
197887
- "",
197888
- fix,
197889
- "",
197890
- `[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)`
197891
- ].join("\n");
197966
+ `${params.provider} ${BYOK_CONFIG_MARKER} ${missing}. ${params.where}`,
197967
+ `\`\`\`yaml
197968
+ ${params.env.join("\n")}
197969
+ \`\`\``,
197970
+ `${params.note ? `${params.note} ` : ""}[${params.provider} setup guide](${params.guide})`
197971
+ ].join("\n\n");
197972
+ }
197973
+ function secretsUrl(params) {
197974
+ return `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197892
197975
  }
197893
197976
  function buildBedrockSetupError(params) {
197894
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197895
- return `Bedrock model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
197896
-
197897
- add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
197898
-
197899
- AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}
197900
- AWS_REGION: \${{ secrets.AWS_REGION }}
197901
- ${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}
197902
-
197903
- \`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.
197904
-
197905
- for full setup instructions, see https://docs.pullfrog.com/bedrock`;
197977
+ return buildSetupError({
197978
+ provider: "Amazon Bedrock",
197979
+ missing: params.missing,
197980
+ where: `Add them to [this repo's Actions secrets](${secretsUrl(params)}) and pass them in the workflow's \`env:\` block:`,
197981
+ env: [
197982
+ `AWS_BEARER_TOKEN_BEDROCK: \${{ secrets.AWS_BEARER_TOKEN_BEDROCK }}`,
197983
+ `AWS_REGION: \${{ secrets.AWS_REGION }}`,
197984
+ `${BEDROCK_MODEL_ID_ENV}: \${{ secrets.${BEDROCK_MODEL_ID_ENV} }}`
197985
+ ],
197986
+ note: "`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (plus `AWS_SESSION_TOKEN` if you use one) can replace the bearer token.",
197987
+ guide: "https://docs.pullfrog.com/bedrock"
197988
+ });
197906
197989
  }
197907
197990
  function buildVertexSetupError(params) {
197908
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197909
- return `Google Vertex AI model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
197910
-
197911
- add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then reference them in your workflow's \`env:\` block:
197912
-
197913
- ${VERTEX_SERVICE_ACCOUNT_JSON_ENV}: \${{ secrets.${VERTEX_SERVICE_ACCOUNT_JSON_ENV} }}
197914
- ${GOOGLE_CLOUD_PROJECT_ENV}: my-project
197915
- ${VERTEX_LOCATION_ENV}: global
197916
- ${VERTEX_MODEL_ID_ENV}: <vertex-model-id>
197917
-
197918
- for full setup instructions, see https://docs.pullfrog.com/vertex`;
197991
+ return buildSetupError({
197992
+ provider: "Google Vertex AI",
197993
+ missing: params.missing,
197994
+ where: `Add the service account to [this repo's Actions secrets](${secretsUrl(params)}) and set the rest in the workflow's \`env:\` block:`,
197995
+ env: [
197996
+ `${VERTEX_SERVICE_ACCOUNT_JSON_ENV}: \${{ secrets.${VERTEX_SERVICE_ACCOUNT_JSON_ENV} }}`,
197997
+ `${GOOGLE_CLOUD_PROJECT_ENV}: my-project`,
197998
+ `${VERTEX_LOCATION_ENV}: global`,
197999
+ `${VERTEX_MODEL_ID_ENV}: <vertex-model-id>`
198000
+ ],
198001
+ guide: "https://docs.pullfrog.com/vertex"
198002
+ });
197919
198003
  }
197920
198004
  function buildOpenAICompatibleSetupError(params) {
197921
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197922
- return `OpenAI-compatible model ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
197923
-
197924
- only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
197925
-
197926
- ${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
197927
- ${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
197928
- ${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
197929
- ${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
197930
- ${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
197931
-
197932
- set the last two to the real limits of the model your endpoint serves. Pullfrog can't
197933
- discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
197934
- capped at 32000 tokens (rejected outright by models with a smaller cap) and
197935
- auto-compaction is disabled, so long runs grow until your endpoint refuses them.
197936
-
197937
- for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
198005
+ return buildSetupError({
198006
+ provider: "OpenAI-compatible endpoint",
198007
+ missing: params.missing,
198008
+ 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)}):`,
198009
+ env: [
198010
+ `${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1`,
198011
+ `${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}`,
198012
+ `${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>`,
198013
+ `${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"`,
198014
+ `${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"`
198015
+ ],
198016
+ 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.",
198017
+ guide: "https://docs.pullfrog.com/openai-compatible"
198018
+ });
197938
198019
  }
197939
198020
  function buildAzureSetupError(params) {
197940
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
197941
- return `Azure OpenAI ${BYOK_CONFIG_MARKER} ${params.missing.join(", ")}.
197942
-
197943
- only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. the rest is plain workflow \`env:\`:
197944
-
197945
- ${AZURE_RESOURCE_NAME_ENV}: <name>
197946
- ${AZURE_API_KEY_ENV}: \${{ secrets.${AZURE_API_KEY_ENV} }}
197947
- ${AZURE_DEPLOYMENT_ENV}: <deployment-name>
197948
- ${AZURE_CONTEXT_ENV}: "400000"
197949
- ${AZURE_MAX_OUTPUT_ENV}: "128000"
197950
-
197951
- ${AZURE_RESOURCE_NAME_ENV} is the \`<name>\` in your endpoint https://<name>.openai.azure.com.
197952
- ${AZURE_DEPLOYMENT_ENV} is the name of the deployment, which is not necessarily the name of
197953
- the model it serves \u2014 Azure routes on the deployment name.
197954
-
197955
- set the last two to the real limits of whichever model your deployment serves. Pullfrog can't
197956
- discover them \u2014 a deployment name carries no catalog metadata \u2014 and without them completions
197957
- are capped at 32000 tokens (rejected outright by models with a smaller cap) and auto-compaction
197958
- is disabled, so long runs grow until Azure refuses them.
197959
-
197960
- for full setup instructions, see https://docs.pullfrog.com/azure`;
198021
+ return buildSetupError({
198022
+ provider: "Azure OpenAI",
198023
+ missing: params.missing,
198024
+ 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)}):`,
198025
+ env: [
198026
+ `${AZURE_RESOURCE_NAME_ENV}: <name>`,
198027
+ `${AZURE_API_KEY_ENV}: \${{ secrets.${AZURE_API_KEY_ENV} }}`,
198028
+ `${AZURE_DEPLOYMENT_ENV}: <deployment-name>`,
198029
+ `${AZURE_CONTEXT_ENV}: "400000"`,
198030
+ `${AZURE_MAX_OUTPUT_ENV}: "128000"`
198031
+ ],
198032
+ 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.`,
198033
+ guide: "https://docs.pullfrog.com/azure"
198034
+ });
197961
198035
  }
197962
198036
  function hasEnvVar2(name) {
197963
198037
  const value2 = process.env[name];
@@ -198117,7 +198191,7 @@ function isMalformedKeyError(text) {
198117
198191
  return /Header '\d+' has invalid value/i.test(text);
198118
198192
  }
198119
198193
  function isClaudeSubscriptionDisabledError(text) {
198120
- return /disabled Claude subscription access/i.test(text);
198194
+ return /disabled Claude subscription access/i.test(text) || /OAuth authentication is currently not allowed for this organization/i.test(text);
198121
198195
  }
198122
198196
  function isKimiPlanTierError(text) {
198123
198197
  return /Your current (?:subscription does not have access to|plan supports only)\b/i.test(text);
@@ -198130,138 +198204,179 @@ var SUBSCRIPTION_CREDENTIALS = {
198130
198204
  CLAUDE_CODE_OAUTH_TOKEN: {
198131
198205
  label: "Claude Pro/Max subscription",
198132
198206
  command: "pullfrog auth claude",
198133
- docs: "https://docs.pullfrog.com/claude-auth"
198207
+ provider: "anthropic"
198134
198208
  },
198135
198209
  CODEX_AUTH_JSON: {
198136
198210
  label: "ChatGPT subscription",
198137
198211
  command: "pullfrog auth codex",
198138
- docs: "https://docs.pullfrog.com/codex-auth"
198212
+ provider: "openai"
198139
198213
  },
198140
198214
  GROK_AUTH_JSON: {
198141
198215
  label: "Grok subscription",
198142
198216
  command: "pullfrog auth grok",
198143
- docs: "https://docs.pullfrog.com/grok-auth"
198217
+ provider: "xai"
198144
198218
  }
198145
198219
  };
198220
+ function subscriptionFor(model) {
198221
+ const provider2 = model?.split("/")[0];
198222
+ for (const [env2, subscription] of Object.entries(SUBSCRIPTION_CREDENTIALS)) {
198223
+ if (subscription.provider === provider2) return { env: env2, ...subscription };
198224
+ }
198225
+ return void 0;
198226
+ }
198227
+ var SIGN_IN_AGAIN = (command) => `Sign in again by running \`${command}\` in a terminal`;
198228
+ var CLAUDE_ORG_DISABLED = {
198229
+ headline: "Your Claude organization doesn't allow subscription sign-in for Claude Code.",
198230
+ detail: "Ask its admin to allow it in the Claude Console, or connect an Anthropic API key instead."
198231
+ };
198232
+ function providerDisplayName(id) {
198233
+ for (const [key, provider2] of Object.entries(providers2)) {
198234
+ if (key === id) return provider2.displayName;
198235
+ }
198236
+ return void 0;
198237
+ }
198146
198238
  function buildRejectedCredentialError(params) {
198147
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
198148
198239
  const subscription = SUBSCRIPTION_CREDENTIALS[params.credential];
198149
- const detail = params.reason ? ` (\`${params.reason}\`)` : "";
198240
+ const details = params.reason ? { summary: "The provider's response", text: params.reason } : void 0;
198150
198241
  if (subscription) {
198151
- return [
198152
- `**Your ${subscription.label} ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
198153
- "",
198154
- `**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.`,
198155
- "",
198156
- `[Re-authenticate \u2192](${subscription.docs}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198157
- ].join("\n");
198158
- }
198159
- 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)`;
198160
- const keysUrl = providerDashboard(params.credential)?.keys;
198161
- const issue3 = keysUrl ? `[Issue a new key \u2192](${keysUrl}) \xB7 ` : "";
198162
- return [
198163
- `**Your \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}**${detail}, so the agent never ran.`,
198164
- "",
198165
- `**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"}.`,
198166
- "",
198167
- `${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)`
198168
- ].join("\n");
198169
- }
198170
- function providerDashboard(credential) {
198242
+ const orgDisabled = params.reason !== void 0 && isClaudeSubscriptionDisabledError(params.reason);
198243
+ return renderFailure({
198244
+ headline: `Your ${subscription.label} ${CREDENTIAL_REJECTED_MARKER}.`,
198245
+ 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.`,
198246
+ details,
198247
+ links: [orgDisabled ? providerLink(params.owner) : pickModelLink(params)]
198248
+ });
198249
+ }
198250
+ const provider2 = credentialProvider(params.credential);
198251
+ const keysUrl = provider2 ? PROVIDER_DASHBOARDS[provider2.id]?.keys : void 0;
198252
+ const update = params.inPullfrogStore ? {
198253
+ label: "Update it in Pullfrog",
198254
+ url: `${getApiUrl()}/console/${params.owner}/${params.name}`
198255
+ } : { label: "Update the Actions secret", url: secretsUrl(params) };
198256
+ return renderFailure({
198257
+ headline: `Your ${provider2 ? `${provider2.displayName} key` : "key"} \`${params.credential}\` ${CREDENTIAL_REJECTED_MARKER}.`,
198258
+ detail: `Issue a new key and update the copy ${params.inPullfrogStore ? "stored in Pullfrog" : "in this repo's GitHub Actions secrets"}.`,
198259
+ details,
198260
+ links: keysUrl ? [{ label: "Issue a new key", url: keysUrl }, update] : [update]
198261
+ });
198262
+ }
198263
+ function credentialProvider(credential) {
198171
198264
  for (const [id, provider2] of Object.entries(providers2)) {
198172
- if (provider2.envVars.includes(credential)) return PROVIDER_DASHBOARDS[id];
198265
+ if (provider2.envVars.includes(credential)) return { id, displayName: provider2.displayName };
198173
198266
  }
198174
198267
  return void 0;
198175
198268
  }
198176
198269
  function buildCredentialPoolRefusedError(params) {
198177
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
198178
- return [
198179
- `**${CREDENTIAL_POOL_REFUSED_MARKER}.** Every credential configured for \`${params.model}\` was rejected or is out of quota, so the agent never ran.`,
198180
- "",
198181
- ...params.refused.map(
198182
- (credential) => `- ${describeRefusal({ ...params, credential, settingsUrl })}`
198183
- ),
198184
- "",
198185
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198186
- ].join("\n");
198270
+ const [first = "", second, ...rest] = params.refused.map(
198271
+ (credential) => describeRefusal({ credential, owner: params.owner, name: params.name })
198272
+ );
198273
+ return renderFailure({
198274
+ headline: `${CREDENTIAL_POOL_REFUSED_MARKER}.`,
198275
+ detail: second === void 0 ? first : `Every credential for \`${params.model}\` was turned down:`,
198276
+ list: second === void 0 ? void 0 : [first, second, ...rest],
198277
+ details: {
198278
+ summary: "The providers' responses",
198279
+ text: params.refused.map((credential) => `${credential.name}: ${credential.verdict.detail}`).join("\n")
198280
+ },
198281
+ links: [pickModelLink(params)]
198282
+ });
198187
198283
  }
198188
198284
  function describeRefusal(params) {
198189
198285
  const credential = params.credential;
198190
198286
  const verdict = credential.verdict;
198191
- const workflow = credential.source === "workflow";
198192
- const head = `\`${credential.name}\` from ${workflow ? "the workflow" : `${credential.source} scope`}: \`${verdict.detail}\` \u2014`;
198193
198287
  const subscription = SUBSCRIPTION_CREDENTIALS[credential.name];
198194
- const dashboard = providerDashboard(credential.name);
198288
+ const dashboard = PROVIDER_DASHBOARDS[credentialProvider(credential.name)?.id ?? ""];
198289
+ const what = subscription ? `Your ${subscription.label}` : `Your \`${credential.name}\``;
198195
198290
  if (verdict.status === "exhausted" && subscription) {
198196
- 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";
198197
- return `${head} ${wait2}, or add an API key for this provider, which Pullfrog falls back to automatically.`;
198291
+ const reset = verdict.resetAt ? `, which resets at **${verdict.resetAt.toISOString().slice(0, 16).replace("T", " ")} UTC**` : "";
198292
+ 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.`;
198293
+ }
198294
+ if (verdict.status === "exhausted") {
198295
+ return `${what} is out of credit or quota. ${dashboard ? `[Add credit](${dashboard.billing})` : "Add credit"} with the provider, or raise its quota.`;
198198
198296
  }
198199
- if (verdict.status === "exhausted")
198200
- return `${head} add credit or raise its quota with the provider.${dashboard ? ` [Top up \u2192](${dashboard.billing})` : ""}`;
198201
- if (subscription)
198202
- 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})`;
198203
- 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})`;
198204
- const issue3 = dashboard?.keys ? `[Issue a new key \u2192](${dashboard.keys}) \xB7 ` : "";
198205
- return `${head} issue a new key and update the copy Pullfrog uses. ${issue3}${where}`;
198297
+ if (subscription && isClaudeSubscriptionDisabledError(verdict.detail)) {
198298
+ return `${CLAUDE_ORG_DISABLED.headline} ${CLAUDE_ORG_DISABLED.detail}`;
198299
+ }
198300
+ if (subscription) return `${what} was rejected. ${SIGN_IN_AGAIN(subscription.command)}.`;
198301
+ const issue3 = dashboard?.keys ? `[Issue a new key](${dashboard.keys})` : "Issue a new key";
198302
+ const where = credential.source === "workflow" ? `in [this repo's GitHub Actions secrets](${secretsUrl(params)})` : `in [Pullfrog](${getApiUrl()}/console/${params.owner}#billing)`;
198303
+ return `${what} was rejected. ${issue3} and update it ${where}.`;
198206
198304
  }
198207
198305
  function formatApiKeyErrorSummary(params) {
198208
198306
  if (params.raw.includes(MISSING_KEY_MARKER)) {
198209
- if (params.raw.startsWith(`**${MISSING_KEY_MARKER}**`)) return params.raw;
198307
+ if (params.raw.startsWith(`**${MISSING_KEY_MARKER}`)) return params.raw;
198210
198308
  return buildMissingApiKeyError({ owner: params.owner, name: params.name });
198211
198309
  }
198212
- const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
198213
- const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
198310
+ const details = params.output ? { summary: "The provider's response", text: params.output } : void 0;
198311
+ const subscription = subscriptionFor(params.model);
198214
198312
  if (isClaudeSessionLimitError(params.raw)) {
198215
198313
  const reset = CLAUDE_SESSION_LIMIT_PATTERN.exec(params.raw)?.[1]?.trim();
198216
- const resets = reset ? ` It resets at **${reset}**.` : "";
198217
- return [
198218
- `**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.`,
198219
- "",
198220
- `[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)`
198221
- ].join("\n");
198314
+ return renderFailure({
198315
+ headline: "Your Claude Pro/Max subscription has hit its usage limit.",
198316
+ detail: `${reset ? `It resets at **${reset}**. ` : ""}Re-trigger after that, or connect an Anthropic API key, which Pullfrog falls back to automatically.`,
198317
+ details,
198318
+ links: [providerLink(params.owner)]
198319
+ });
198222
198320
  }
198223
198321
  if (isMalformedKeyError(params.raw)) {
198224
- return [
198225
- "**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.",
198226
- "",
198227
- `[Pullfrog secrets \u2192](${getApiUrl()}/console/${params.owner}) \xB7 [Repo secrets \u2192](${githubSecretsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198228
- ].join("\n");
198322
+ return renderFailure({
198323
+ headline: "Your stored API key can't be sent as an HTTP header.",
198324
+ 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.",
198325
+ links: [
198326
+ {
198327
+ label: "Pullfrog provider settings",
198328
+ url: `${getApiUrl()}/console/${params.owner}#billing`
198329
+ },
198330
+ { label: "GitHub Actions secrets", url: secretsUrl(params) }
198331
+ ]
198332
+ });
198229
198333
  }
198230
198334
  if (isClaudeSubscriptionDisabledError(params.raw)) {
198231
- return [
198232
- `**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.`,
198233
- "",
198234
- `[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)`
198235
- ].join("\n");
198335
+ return renderFailure({
198336
+ ...CLAUDE_ORG_DISABLED,
198337
+ details,
198338
+ links: [providerLink(params.owner)]
198339
+ });
198236
198340
  }
198237
198341
  if (isKimiPlanTierError(params.raw)) {
198238
- return [
198239
- "**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.",
198240
- "",
198241
- `[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)`
198242
- ].join("\n");
198342
+ return renderFailure({
198343
+ headline: "Your Kimi membership doesn't include the model this run asked for.",
198344
+ 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.",
198345
+ details,
198346
+ links: [
198347
+ { label: "Kimi plans", url: "https://www.kimi.com/membership/pricing" },
198348
+ pickModelLink(params)
198349
+ ]
198350
+ });
198243
198351
  }
198244
198352
  if (isOAuthCredentialExpiredError(params.raw)) {
198245
- return [
198246
- `**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.`,
198247
- "",
198248
- `[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)`
198249
- ].join("\n");
198250
- }
198251
- const subscription = Object.keys(SUBSCRIPTION_CREDENTIALS).find(hasEnvVar2);
198252
- if (subscription && !hasEnvVar2("ANTHROPIC_API_KEY")) {
198253
- const details = SUBSCRIPTION_CREDENTIALS[subscription];
198254
- return [
198255
- `**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.`,
198256
- "",
198257
- `[Re-authenticate \u2192](${details?.docs}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198258
- ].join("\n");
198259
- }
198260
- return [
198261
- `**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.`,
198262
- "",
198263
- `[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)`
198264
- ].join("\n");
198353
+ return renderFailure({
198354
+ headline: subscription ? `Your ${subscription.label}'s sign-in has expired or been revoked.` : "Your provider sign-in has expired or been revoked.",
198355
+ 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.",
198356
+ details,
198357
+ links: [pickModelLink(params)]
198358
+ });
198359
+ }
198360
+ if (subscription && hasEnvVar2(subscription.env) && params.credential !== "api_key") {
198361
+ return renderFailure({
198362
+ headline: `Your ${subscription.label} was rejected during this run.`,
198363
+ detail: `${SIGN_IN_AGAIN(subscription.command)}, then re-trigger.`,
198364
+ details,
198365
+ links: [pickModelLink(params)]
198366
+ });
198367
+ }
198368
+ return renderFailure({
198369
+ headline: `Your ${providerDisplayName(params.model?.split("/")[0]) ?? "model provider"} API key was rejected.`,
198370
+ detail: "Issue a new key with the provider, then update the copy Pullfrog uses, in Pullfrog or in this repo's GitHub Actions secrets.",
198371
+ details,
198372
+ links: [
198373
+ {
198374
+ label: "Pullfrog provider settings",
198375
+ url: `${getApiUrl()}/console/${params.owner}#billing`
198376
+ },
198377
+ { label: "GitHub Actions secrets", url: secretsUrl(params) }
198378
+ ]
198379
+ });
198265
198380
  }
198266
198381
 
198267
198382
  // utils/agent.ts
@@ -198287,7 +198402,7 @@ function resolveSlug(slug2) {
198287
198402
  const bedrockId = process.env[BEDROCK_MODEL_ID_ENV]?.trim();
198288
198403
  if (!bedrockId) {
198289
198404
  throw new Error(
198290
- `${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.`
198405
+ `\`${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)`
198291
198406
  );
198292
198407
  }
198293
198408
  return bedrockId;
@@ -198296,7 +198411,7 @@ function resolveSlug(slug2) {
198296
198411
  const vertexId = process.env[VERTEX_MODEL_ID_ENV]?.trim();
198297
198412
  if (!vertexId) {
198298
198413
  throw new Error(
198299
- `${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.`
198414
+ `\`${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)`
198300
198415
  );
198301
198416
  }
198302
198417
  return vertexId;
@@ -198305,7 +198420,7 @@ function resolveSlug(slug2) {
198305
198420
  const deployment = process.env[AZURE_DEPLOYMENT_ENV]?.trim();
198306
198421
  if (!deployment) {
198307
198422
  throw new Error(
198308
- `${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.`
198423
+ `\`${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)`
198309
198424
  );
198310
198425
  }
198311
198426
  return `${AZURE_PROVIDER}/${deployment}`;
@@ -198314,7 +198429,7 @@ function resolveSlug(slug2) {
198314
198429
  const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
198315
198430
  if (!modelId) {
198316
198431
  throw new Error(
198317
- `${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.`
198432
+ `\`${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)`
198318
198433
  );
198319
198434
  }
198320
198435
  return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
@@ -198386,9 +198501,12 @@ var BillingError = class extends Error {
198386
198501
  }
198387
198502
  };
198388
198503
  var TransientError = class extends Error {
198389
- constructor(message2) {
198504
+ /** the customer-facing follow-up to `message`, which is the headline. */
198505
+ detail;
198506
+ 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).") {
198390
198507
  super(message2);
198391
198508
  this.name = "TransientError";
198509
+ this.detail = detail;
198392
198510
  }
198393
198511
  };
198394
198512
  function billingConsoleUrl(owner) {
@@ -198423,41 +198541,34 @@ function formatCommercialGateSummary(params) {
198423
198541
  });
198424
198542
  }
198425
198543
  function formatBillingErrorSummary(error49, owner) {
198544
+ const billing = { label: "Add credit", url: billingConsoleUrl(owner) };
198426
198545
  if (error49.code === "router_requires_card") {
198427
- return [
198428
- "**Your Pullfrog Router balance is empty.**",
198429
- "",
198430
- "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.",
198431
- "",
198432
- `[Add a card to top up \u2192](${billingConsoleUrl(owner)}) \xB7 [Bring your own key \u2192](${billingConsoleUrl(owner)})`
198433
- ].join("\n");
198546
+ return renderFailure({
198547
+ headline: "Your Pullfrog Router balance is empty.",
198548
+ detail: "Add a card to top it up, or connect your own provider key. Router usage is billed at provider cost with no markup.",
198549
+ links: [{ label: "Billing and providers", url: billingConsoleUrl(owner) }]
198550
+ });
198434
198551
  }
198435
198552
  if (error49.code === "router_balance_exhausted") {
198436
- return [
198437
- "**Your Pullfrog Router balance is exhausted.**",
198438
- "",
198439
- "You have a payment method on file but auto-reload is disabled, so runs paused once your balance went past the overdraft buffer.",
198440
- "",
198441
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner)})`
198442
- ].join("\n");
198553
+ return renderFailure({
198554
+ headline: "Your Pullfrog Router balance is used up.",
198555
+ detail: "Auto-reload is off, so runs paused once the balance passed its overdraft buffer. Add credit or turn on auto-reload.",
198556
+ links: [billing]
198557
+ });
198443
198558
  }
198444
198559
  if (error49.code === "router_keylimit_exhausted") {
198445
- return [
198446
- "**This run was cut short: your Pullfrog Router balance ran out mid-run.**",
198447
- "",
198448
- "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.",
198449
- "",
198450
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Enable auto-reload \u2192](${billingConsoleUrl(owner)})`
198451
- ].join("\n");
198560
+ return renderFailure({
198561
+ headline: "Your Pullfrog Router balance ran out partway through this run.",
198562
+ detail: "Add credit or turn on auto-reload to keep runs going.",
198563
+ links: [billing]
198564
+ });
198452
198565
  }
198453
198566
  if (error49.code === "router_monthly_limit") {
198454
- return [
198455
- "**Pullfrog Router is out of credit, and the monthly spend limit blocks the next auto-reload.**",
198456
- "",
198457
- "Top up by hand, ask your admin to raise the cap, or wait for it to reset at 00:00 UTC on the 1st.",
198458
- "",
198459
- `[Top up balance \u2192](${billingConsoleUrl(owner)}) \xB7 [Adjust limit \u2192](${billingConsoleUrl(owner)})`
198460
- ].join("\n");
198567
+ return renderFailure({
198568
+ headline: "Your Pullfrog Router balance is empty, and the monthly spend limit blocks the next auto-reload.",
198569
+ 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.",
198570
+ links: [billing]
198571
+ });
198461
198572
  }
198462
198573
  if (error49.code === "commercial_plan_required") {
198463
198574
  return formatCommercialGateSummary({
@@ -198479,39 +198590,27 @@ function formatBillingErrorSummary(error49, owner) {
198479
198590
  }
198480
198591
  if (error49.needsReauthentication) {
198481
198592
  const code = error49.declineCode ?? "authentication_required";
198482
- return [
198483
- `**Your card issuer requires 3D Secure on every charge** (\`${code}\`).`,
198484
- "",
198485
- "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.",
198486
- "",
198487
- `[Top up balance \u2192](${billingConsoleUrl(owner)})`
198488
- ].join("\n");
198593
+ return renderFailure({
198594
+ headline: `Your card issuer requires 3D Secure on every charge (\`${code}\`).`,
198595
+ 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.",
198596
+ links: [billing]
198597
+ });
198489
198598
  }
198490
198599
  if (error49.declineCode) {
198491
- return [
198492
- `**Your card was declined** (\`${error49.declineCode}\`).`,
198493
- "",
198494
- "Update your payment method and Pullfrog will retry on the next run.",
198495
- "",
198496
- `[Update payment method \u2192](${billingConsoleUrl(owner)})`
198497
- ].join("\n");
198600
+ return renderFailure({
198601
+ headline: `Your card was declined (\`${error49.declineCode}\`).`,
198602
+ detail: "Update your payment method, and Pullfrog will retry on the next run.",
198603
+ links: [{ label: "Update payment method", url: billingConsoleUrl(owner) }]
198604
+ });
198498
198605
  }
198499
- return [
198500
- "**Your Pullfrog balance is empty.**",
198501
- "",
198502
- "Top up your balance or enable auto-reload to keep runs flowing.",
198503
- "",
198504
- `[Manage billing \u2192](${billingConsoleUrl(owner)})`
198505
- ].join("\n");
198606
+ return renderFailure({
198607
+ headline: "Your Pullfrog balance is empty.",
198608
+ detail: "Add credit or turn on auto-reload to keep runs going.",
198609
+ links: [billing]
198610
+ });
198506
198611
  }
198507
- function formatTransientErrorSummary(error49, owner) {
198508
- return [
198509
- "**Pullfrog billing is temporarily unavailable.**",
198510
- "",
198511
- error49.message,
198512
- "",
198513
- `Usually transient; the next dispatch should succeed. If it persists, check [status.pullfrog.com](https://status.pullfrog.com) or [your console](${billingConsoleUrl(owner)}).`
198514
- ].join("\n");
198612
+ function formatTransientErrorSummary(error49) {
198613
+ return renderFailure({ headline: error49.message, detail: error49.detail });
198515
198614
  }
198516
198615
 
198517
198616
  // utils/credentialCheck.ts
@@ -198841,10 +198940,7 @@ var select2 = mutation({
198841
198940
  signal: ctx.signal
198842
198941
  });
198843
198942
  if (response.status === 404) return null;
198844
- if (!response.ok)
198845
- throw new Error("could not load a configured credential; no fallback was charged", {
198846
- cause: response.status
198847
- });
198943
+ if (!response.ok) throw new Error(buildSecretsUnavailableError(), { cause: response.status });
198848
198944
  return selectedCredentialSchema.parse(await response.json());
198849
198945
  },
198850
198946
  retry: (error49, attempt) => error49 instanceof Error && typeof error49.cause === "number" && error49.cause >= 400 && error49.cause < 500 ? -1 : delay2([250, 1e3], attempt),
@@ -199658,26 +199754,16 @@ function ossAllowedLabels() {
199658
199754
  return ossFundedModelNames().map((name) => `\`${name}\``).join(", ");
199659
199755
  }
199660
199756
  function buildModelAccessError(input) {
199661
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
199662
- const secretsUrl = `https://github.com/${input.owner}/${input.name}/settings/secrets/actions`;
199663
- const docsUrl = "https://docs.pullfrog.com/keys";
199664
- const headline = `**The ${MODEL_ACCESS_MARKER}: \`${input.model}\`.**`;
199665
- const branch = {
199666
- oss: {
199667
- 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.`,
199668
- cta: `[Configure model \u2192](${settingsUrl}) \xB7 [Add a provider key \u2192](${secretsUrl}) \xB7 [Setup docs \u2192](${docsUrl})`
199669
- },
199670
- byok_no_key: {
199671
- 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.`,
199672
- cta: `[Open repo secrets \u2192](${secretsUrl}) \xB7 [Configure model \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](${docsUrl})`
199673
- },
199674
- router: {
199675
- why: "Pullfrog Router can't serve this model. Pick a Router-supported model, or add your own provider key to run it directly.",
199676
- cta: `[Configure model \u2192](${settingsUrl}) \xB7 [Add a provider key \u2192](${secretsUrl})`
199677
- }
199757
+ const why = {
199758
+ 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.`,
199759
+ byok_no_key: `This run has no API key for \`${input.model}\`. Connect one, or pick a model you already have a key for.`,
199760
+ router: "Pullfrog Router can't serve this model. Pick a Router-supported model, or connect your own provider to run it directly."
199678
199761
  };
199679
- const { why, cta } = branch[input.reason];
199680
- return [headline, "", why, "", cta].join("\n");
199762
+ return renderFailure({
199763
+ headline: `The ${MODEL_ACCESS_MARKER}: \`${input.model}\`.`,
199764
+ detail: why[input.reason],
199765
+ links: [pickModelLink(input), providerLink(input.owner)]
199766
+ });
199681
199767
  }
199682
199768
 
199683
199769
  // utils/overrides.ts
@@ -200234,9 +200320,6 @@ async function revokeGitHubInstallationToken(token) {
200234
200320
 
200235
200321
  // utils/errorReport.ts
200236
200322
  async function reportErrorToComment(ctx) {
200237
- const formattedError = ctx.title ? `${ctx.title}
200238
-
200239
- ${ctx.error}` : ctx.error;
200240
200323
  const repoContext = parseRepoContext();
200241
200324
  const octokit = createOctokit(getGitHubInstallationToken(), getMcpTokenRefresh());
200242
200325
  const runId = process.env.GITHUB_RUN_ID ? Number.parseInt(process.env.GITHUB_RUN_ID, 10) : void 0;
@@ -200255,9 +200338,10 @@ ${ctx.error}` : ctx.error;
200255
200338
  clamped: ctx.toolState.modelClamped,
200256
200339
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
200257
200340
  oss: ctx.toolState.oss,
200258
- owner: repoContext.owner
200341
+ owner: repoContext.owner,
200342
+ failed: true
200259
200343
  });
200260
- const body = `${formattedError}${footer}`;
200344
+ const body = `${redactSecrets(ctx.error)}${footer}`;
200261
200345
  const comment = ctx.toolState.progressComment;
200262
200346
  if (comment) {
200263
200347
  await updateProgressComment(
@@ -200297,8 +200381,9 @@ async function mintProxyKey(ctx) {
200297
200381
  const r2 = await apiFetch({ path: "/api/proxy-token", method: "POST", headers });
200298
200382
  if (r2.status >= 500) {
200299
200383
  const body = await r2.json().catch(() => null);
200384
+ log2.warning(`proxy key mint: ${r2.status} ${body?.error ?? ""}`);
200300
200385
  throw new TransientError(
200301
- body?.error ?? "billing service temporarily unavailable \u2014 retry shortly"
200386
+ "Pullfrog's billing service is temporarily unavailable, so this run didn't start."
200302
200387
  );
200303
200388
  }
200304
200389
  return r2;
@@ -200316,7 +200401,8 @@ async function mintProxyKey(ctx) {
200316
200401
  }
200317
200402
  if (response.status === 404) {
200318
200403
  throw new TransientError(
200319
- "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."
200404
+ "Pullfrog couldn't match this repository to its account.",
200405
+ "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."
200320
200406
  );
200321
200407
  }
200322
200408
  if (!response.ok) {
@@ -200425,7 +200511,7 @@ async function runProxyResolution(ctx) {
200425
200511
  throw error49;
200426
200512
  }
200427
200513
  if (error49 instanceof TransientError) {
200428
- const summary3 = formatTransientErrorSummary(error49, ctx.repo.owner);
200514
+ const summary3 = formatTransientErrorSummary(error49);
200429
200515
  await writeSummary(summary3).catch(() => {
200430
200516
  });
200431
200517
  await reportErrorToComment({
@@ -200450,13 +200536,14 @@ async function resolveTrialFallback(ctx) {
200450
200536
  if (!key) return false;
200451
200537
  process.env.OPENROUTER_API_KEY = key;
200452
200538
  maskSecret(key);
200453
- ctx.payload.proxyModel = DEFAULT_PROXY_MODEL;
200454
- ctx.toolState.model = DEFAULT_PROXY_MODEL;
200539
+ ctx.payload.proxyModel = TRIAL_PROXY_MODEL;
200540
+ ctx.toolState.model = TRIAL_PROXY_MODEL;
200455
200541
  ctx.toolState.modelClamped = {
200456
- from: ctx.configuredModel ?? DEFAULT_PROXY_MODEL,
200457
- reason: "trial"
200542
+ from: ctx.configuredModel ?? TRIAL_PROXY_MODEL,
200543
+ reason: "trial",
200544
+ creditsLeftUsd: ctx.creditsLeftUsd
200458
200545
  };
200459
- log2.info(`\xBB proxy: trial \u2192 ${DEFAULT_PROXY_MODEL}`);
200546
+ log2.info(`\xBB proxy: trial \u2192 ${TRIAL_PROXY_MODEL}`);
200460
200547
  return true;
200461
200548
  }
200462
200549
 
@@ -200569,14 +200656,27 @@ ${collapsible}` : output;
200569
200656
  return { success: true, output: ctx.result.output || "" };
200570
200657
  } catch (writeError) {
200571
200658
  const error50 = `failed to deliver agent result: ${getErrorMessage2(writeError)}`;
200572
- await reportErrorToComment({ toolState, error: error50, title: "Error" }).catch(() => {
200659
+ await reportErrorToComment({
200660
+ toolState,
200661
+ error: renderFailure({
200662
+ headline: "Pullfrog couldn't post its answer to GitHub.",
200663
+ detail: "GitHub refused the write; its error is below. Re-trigger to try again.",
200664
+ error: getErrorMessage2(writeError)
200665
+ })
200666
+ }).catch(() => {
200573
200667
  });
200574
200668
  return { success: false, error: error50, output: ctx.result.output || "" };
200575
200669
  }
200576
200670
  }
200577
200671
  const error49 = ctx.result.error || "agent completed without reporting progress";
200578
200672
  try {
200579
- await reportErrorToComment({ toolState, error: error49, title: "Error" });
200673
+ await reportErrorToComment({
200674
+ toolState,
200675
+ error: renderFailure({
200676
+ headline: "The agent finished without posting anything.",
200677
+ detail: "Re-trigger to try again."
200678
+ })
200679
+ });
200580
200680
  } catch {
200581
200681
  }
200582
200682
  return { success: false, error: error49, output: ctx.result.output || "" };
@@ -200637,63 +200737,57 @@ async function resolveRunContextData(params) {
200637
200737
  // outcome as the server-side failure: the run never sees stored secrets.
200638
200738
  secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0,
200639
200739
  routerUnfunded: runContext.routerUnfunded,
200640
- trialFallback: runContext.trialFallback
200740
+ trialFallback: runContext.trialFallback,
200741
+ trialCreditsLeftUsd: runContext.trialCreditsLeftUsd
200641
200742
  };
200642
200743
  }
200643
200744
 
200644
200745
  // utils/agentHangReport.ts
200645
200746
  var MAX_STDERR_BYTES = 3e3;
200646
200747
  function formatAgentHangBody(input) {
200647
- if (!input.diagnostic) return null;
200648
- if (input.diagnostic.lastProviderError === "provider billing exhausted") {
200649
- return formatBillingExhaustedBody(input.diagnostic);
200650
- }
200651
- const verb = input.isHang ? "stalled" : "failed";
200652
- const cause = input.diagnostic.lastProviderError ? ` \u2014 likely cause: \`${input.diagnostic.lastProviderError}\`` : "";
200653
- const headline = `**${input.diagnostic.label} ${verb}**${cause}`;
200654
- const explanation = formatExplanation({
200655
- isHang: input.isHang,
200656
- errorMessage: input.errorMessage,
200657
- idleSec: input.diagnostic.idleSec
200658
- });
200659
- const parts = [headline, "", `${explanation} ${formatEventsPart(input.diagnostic)}`];
200660
- const tail = renderStderrTail(input.diagnostic.recentStderr);
200661
- if (tail) {
200662
- const fence = pickFence(tail);
200663
- parts.push(
200664
- "",
200665
- "<details><summary>Recent agent stderr</summary>",
200666
- "",
200667
- fence,
200668
- tail,
200669
- fence,
200670
- "",
200671
- "</details>"
200672
- );
200748
+ const diagnostic = input.diagnostic;
200749
+ if (!diagnostic) return null;
200750
+ const output = renderStderrTail(diagnostic.recentStderr);
200751
+ const details = output ? { summary: "Last agent output", text: output } : void 0;
200752
+ const idle = formatIdle(diagnostic.idleSec ?? parseIdleSec(input.errorMessage));
200753
+ if (diagnostic.lastProviderError === PROVIDER_BILLING_EXHAUSTED_LABEL) {
200754
+ const billingUrl = extractBillingUrl(diagnostic.recentStderr);
200755
+ return renderFailure({
200756
+ headline: "Your model provider's account is out of credit.",
200757
+ detail: "The provider kept refusing the request, so Pullfrog stopped the run. Top up the account, then re-trigger.",
200758
+ details,
200759
+ links: billingUrl ? [{ label: "Top up", url: billingUrl }] : []
200760
+ });
200673
200761
  }
200674
- return parts.join("\n");
200675
- }
200676
- function formatExplanation(input) {
200677
- if (!input.isHang) return `The agent exited unexpectedly: ${input.errorMessage}`;
200678
- const idleSec = input.idleSec ?? parseIdleSec(input.errorMessage);
200679
- if (idleSec === void 0) {
200680
- return "The agent stopped emitting events and was killed by the activity-timeout watchdog.";
200762
+ if (diagnostic.lastProviderError) {
200763
+ return renderFailure({
200764
+ headline: "The model provider kept returning errors, so Pullfrog stopped this run.",
200765
+ detail: `The last one was \`${diagnostic.lastProviderError}\`. Re-trigger to try again.`,
200766
+ details
200767
+ });
200768
+ }
200769
+ if (!diagnostic.sawModelOutput) {
200770
+ return renderFailure({
200771
+ headline: `The model never responded, so Pullfrog stopped this run${idle ? ` after ${idle}` : ""}.`,
200772
+ detail: "The request went out but nothing came back. This is usually temporary, so re-trigger to try again.",
200773
+ details
200774
+ });
200681
200775
  }
200682
- return `The agent stopped emitting events for ${idleSec}s and was killed by the activity-timeout watchdog.`;
200776
+ return renderFailure({
200777
+ headline: `The agent went quiet${idle ? ` for ${idle}` : ""}, so Pullfrog stopped this run.`,
200778
+ detail: "Re-trigger to try again.",
200779
+ details
200780
+ });
200781
+ }
200782
+ function formatIdle(seconds) {
200783
+ if (seconds === void 0) return void 0;
200784
+ if (seconds < 90) return `${seconds} seconds`;
200785
+ return `${Math.round(seconds / 60)} minutes`;
200683
200786
  }
200684
200787
  function parseIdleSec(message2) {
200685
200788
  const match3 = /no output for (\d+)s/.exec(message2);
200686
200789
  return match3 ? Number(match3[1]) : void 0;
200687
200790
  }
200688
- function formatEventsPart(diagnostic) {
200689
- if (diagnostic.lastProviderError) {
200690
- return `${diagnostic.eventCount} events were processed before the failure.`;
200691
- }
200692
- if (!diagnostic.sawModelOutput) {
200693
- 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.";
200694
- }
200695
- return `${diagnostic.eventCount} events were processed before the failure.`;
200696
- }
200697
200791
  function renderStderrTail(lines) {
200698
200792
  if (lines.length === 0) return "";
200699
200793
  const joined = lines.join("\n");
@@ -200701,13 +200795,6 @@ function renderStderrTail(lines) {
200701
200795
  return `... (older lines truncated)
200702
200796
  ${joined.slice(-MAX_STDERR_BYTES)}`;
200703
200797
  }
200704
- function pickFence(content) {
200705
- let max = 0;
200706
- for (const match3 of content.matchAll(/`+/g)) {
200707
- if (match3[0].length > max) max = match3[0].length;
200708
- }
200709
- return "`".repeat(Math.max(3, max + 1));
200710
- }
200711
200798
  function extractBillingUrl(lines) {
200712
200799
  const urlPattern = /https:\/\/(?:opencode\.ai\/[^\s"]*billing[^\s"]*|console\.anthropic\.com[^\s"]*|console\.cloud\.google\.com[^\s"]*billing[^\s"]*)/i;
200713
200800
  for (let i2 = lines.length - 1; i2 >= 0; i2--) {
@@ -200716,28 +200803,6 @@ function extractBillingUrl(lines) {
200716
200803
  }
200717
200804
  return void 0;
200718
200805
  }
200719
- function formatBillingExhaustedBody(diagnostic) {
200720
- const headline = `**${diagnostic.label} stopped** \u2014 your model provider returned a billing-exhausted response.`;
200721
- const billingUrl = extractBillingUrl(diagnostic.recentStderr);
200722
- 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.";
200723
- 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.";
200724
- const parts = [headline, "", explanation, "", cta];
200725
- const tail = renderStderrTail(diagnostic.recentStderr);
200726
- if (tail) {
200727
- const fence = pickFence(tail);
200728
- parts.push(
200729
- "",
200730
- "<details><summary>Recent agent stderr</summary>",
200731
- "",
200732
- fence,
200733
- tail,
200734
- fence,
200735
- "",
200736
- "</details>"
200737
- );
200738
- }
200739
- return parts.join("\n");
200740
- }
200741
200806
 
200742
200807
  // utils/runErrorRenderer.ts
200743
200808
  function isProviderModelNotFoundError(message2) {
@@ -200752,84 +200817,9 @@ function isNoProviderAvailableError(message2) {
200752
200817
  function isZenFreeTierGateError(message2) {
200753
200818
  return /free tier can only be used from within OpenCode/i.test(message2);
200754
200819
  }
200755
- function formatZenFreeTierGateSummary(input) {
200756
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
200757
- return [
200758
- "**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.",
200759
- "",
200760
- '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.',
200761
- "",
200762
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200763
- "",
200764
- `\`\`\`
200765
- ${input.raw}
200766
- \`\`\``
200767
- ].join("\n");
200768
- }
200769
- function formatNoProviderAvailableSummary(input) {
200770
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
200771
- return [
200772
- "**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.",
200773
- "",
200774
- "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.",
200775
- "",
200776
- `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200777
- "",
200778
- `\`\`\`
200779
- ${input.raw}
200780
- \`\`\``
200781
- ].join("\n");
200782
- }
200783
- function isRunTimeoutError(message2) {
200784
- return message2.startsWith("agent run timed out after");
200785
- }
200786
- function formatRunTimeoutSummary(input) {
200787
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
200788
- return [
200789
- "**This run hit its time limit while still working, so it stopped before finishing.**",
200790
- "",
200791
- "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.",
200792
- "",
200793
- `[Repo settings \u2192](${settingsUrl}) \xB7 [Flags \u2192](https://docs.pullfrog.com/flags)`,
200794
- "",
200795
- `\`\`\`
200796
- ${input.raw}
200797
- \`\`\``
200798
- ].join("\n");
200799
- }
200800
- function formatContextOverflowSummary(input) {
200801
- const settingsUrl = `${getApiUrl()}/console/${input.owner}/${input.name}`;
200802
- return [
200803
- "**This run exceeded the model's context window.** Pullfrog read more than the model could hold, so it stopped before finishing.",
200804
- "",
200805
- `Pick a model with a larger context window, or split this PR into smaller ones and re-trigger.`,
200806
- "",
200807
- `[Model settings \u2192](${settingsUrl}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200808
- "",
200809
- `\`\`\`
200810
- ${input.raw}
200811
- \`\`\``
200812
- ].join("\n");
200813
- }
200814
- function formatGenericFailure(errorMessage) {
200815
- return [
200816
- "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.",
200817
- "",
200818
- "```",
200819
- errorMessage,
200820
- "```"
200821
- ].join("\n");
200822
- }
200823
- function formatMinimalFailureComment(repo) {
200824
- const runId = process.env.GITHUB_RUN_ID;
200825
- if (!runId) return "**Run failed.**";
200826
- const server = process.env.GITHUB_SERVER_URL ?? "https://github.com";
200827
- const url4 = `${server}/${repo.owner}/${repo.name}/actions/runs/${runId}`;
200828
- return `**Run failed.** [View the logs \u2192](${url4})`;
200829
- }
200830
- var PROVIDER_BILLING_URLS = Object.fromEntries(
200831
- Object.entries(PROVIDER_DASHBOARDS).map(([id, dashboard]) => [id, dashboard.billing])
200832
- );
200820
+ var RUN_TIMEOUT_PATTERN = /^agent run timed out after (\S+)/;
200821
+ var STOP_SCRIPT_PATTERN = /^stop hook failed \(exit code (-?\d+)\): ([\s\S]*)$/;
200822
+ var UNSUBMITTED_REVIEW_PATTERN = /^\w+ mode finished without calling /;
200833
200823
  function detectProviderId(message2) {
200834
200824
  const harnessId = extractProviderId(message2);
200835
200825
  if (harnessId) return harnessId;
@@ -200841,292 +200831,242 @@ function detectProviderId(message2) {
200841
200831
  if (/platform\.openai\.com/i.test(message2)) return "openai";
200842
200832
  return null;
200843
200833
  }
200844
- var OPENROUTER_PRIVACY_URL = "https://openrouter.ai/settings/privacy";
200845
- function formatProviderBillingExhausted(input) {
200846
- const providerId = detectProviderId(input.errorMessage);
200847
- const dashboardUrl = providerId ? PROVIDER_BILLING_URLS[providerId] : void 0;
200848
- if (isOpenRouterKeyLimitExceeded(input.errorMessage)) {
200849
- return [
200850
- "**Your OpenRouter key's total limit is too low for this request.**",
200851
- "",
200852
- "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.",
200853
- "",
200854
- `[Raise the key's limit \u2192](${PROVIDER_DASHBOARDS.openrouter.keys}) \xB7 [Add credits \u2192](${PROVIDER_BILLING_URLS.openrouter})`,
200855
- "",
200856
- `\`\`\`
200857
- ${input.errorMessage}
200858
- \`\`\``
200859
- ].join("\n");
200860
- }
200861
- const spendCap = findAnthropicSpendCap(input.errorMessage);
200862
- if (spendCap) {
200863
- const regains = spendCap.regainAt ? ` Access returns on **${spendCap.regainAt}**.` : "";
200864
- return [
200865
- `**Your \`anthropic\` account has reached its configured API usage limit.**${regains}`,
200866
- "",
200867
- "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.",
200868
- "",
200869
- `[Anthropic billing settings \u2192](${PROVIDER_BILLING_URLS.anthropic})`,
200870
- "",
200871
- `\`\`\`
200872
- ${input.errorMessage}
200873
- \`\`\``
200874
- ].join("\n");
200834
+ function resolveAccount(input) {
200835
+ const subscription = input.credential === "subscription" ? subscriptionFor(input.model) : void 0;
200836
+ if (subscription) {
200837
+ const plan = Object.values(PROVIDER_DASHBOARDS).find(
200838
+ (dashboard2) => dashboard2.credential === subscription.env
200839
+ );
200840
+ return {
200841
+ name: plan?.name ?? null,
200842
+ phrase: subscription.label,
200843
+ billingUrl: plan?.billing,
200844
+ subscription: true
200845
+ };
200875
200846
  }
200876
- const headline = providerId ? `**Your \`${providerId}\` account is out of credit.**` : "**Your provider account is out of credit.**";
200877
- const cta = dashboardUrl ? `[Top up \`${providerId}\` \u2192](${dashboardUrl})` : "Top up your provider account, then re-trigger Pullfrog.";
200878
- return [
200879
- headline,
200880
- "",
200881
- "Pullfrog detected a billing-exhausted response from your provider \u2014 the agent stopped before completing this run.",
200882
- "",
200883
- cta,
200884
- "",
200885
- `\`\`\`
200886
- ${input.errorMessage}
200887
- \`\`\``
200888
- ].join("\n");
200847
+ const id = detectProviderId(input.errorMessage) ?? input.model?.split("/")[0] ?? null;
200848
+ const dashboard = id ? PROVIDER_DASHBOARDS[id] : void 0;
200849
+ const name = dashboard?.name ?? providerDisplayName(id) ?? null;
200850
+ return {
200851
+ name,
200852
+ phrase: name ? `${name} account` : "model provider account",
200853
+ billingUrl: dashboard?.billing,
200854
+ subscription: false
200855
+ };
200889
200856
  }
200890
- function formatTransientUpstreamBody(errorMessage) {
200891
- const providerId = detectProviderId(errorMessage);
200892
- const who = providerId ? `\`${providerId}\`` : "the model provider";
200893
- return [
200894
- `**${who} had a temporary upstream failure.**`,
200895
- "",
200896
- "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.",
200897
- "",
200898
- `\`\`\`
200899
- ${errorMessage}
200900
- \`\`\``
200901
- ].join("\n");
200857
+ function providerResponse(ctx) {
200858
+ return {
200859
+ summary: ctx.account.name ? `${ctx.account.name}'s response` : "The provider's response",
200860
+ text: ctx.errorMessage
200861
+ };
200902
200862
  }
200903
- function formatProviderNoRoutableEndpoints(input) {
200904
- return [
200905
- "**OpenRouter can't route this model anywhere your account allows.**",
200906
- "",
200907
- "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.",
200908
- "",
200909
- `[OpenRouter data policy \u2192](${OPENROUTER_PRIVACY_URL}) \xB7 [Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
200910
- "",
200911
- `\`\`\`
200912
- ${input.errorMessage}
200913
- \`\`\``
200914
- ].join("\n");
200863
+ function formatProviderBillingExhausted(ctx) {
200864
+ if (isOpenRouterKeyLimitExceeded(ctx.errorMessage)) {
200865
+ const openrouter = PROVIDER_DASHBOARDS.openrouter;
200866
+ return renderFailure({
200867
+ headline: "Your OpenRouter key's spending limit is too low for this request.",
200868
+ 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.",
200869
+ details: providerResponse(ctx),
200870
+ links: openrouter?.keys ? [
200871
+ { label: "Raise the key's limit", url: openrouter.keys },
200872
+ { label: "Add credit", url: openrouter.billing }
200873
+ ] : []
200874
+ });
200875
+ }
200876
+ const spendCap = findAnthropicSpendCap(ctx.errorMessage);
200877
+ if (spendCap) {
200878
+ const regains = spendCap.regainAt ? `Access returns on **${spendCap.regainAt}**. ` : "";
200879
+ const anthropic = PROVIDER_DASHBOARDS.anthropic;
200880
+ return renderFailure({
200881
+ headline: "Your Anthropic account has reached its API spend limit.",
200882
+ detail: `${regains}This is a limit set in your Anthropic Console, not an empty balance; raise or remove it to run before then.`,
200883
+ details: providerResponse(ctx),
200884
+ links: anthropic ? [{ label: "Anthropic billing settings", url: anthropic.billing }] : []
200885
+ });
200886
+ }
200887
+ return renderFailure({
200888
+ headline: `Your ${ctx.account.phrase} is out of credit.`,
200889
+ detail: "Top it up, then re-trigger Pullfrog.",
200890
+ details: providerResponse(ctx),
200891
+ links: ctx.account.billingUrl ? [{ label: `Top up ${ctx.account.name}`, url: ctx.account.billingUrl }] : []
200892
+ });
200893
+ }
200894
+ function formatProviderUsageLimit(ctx) {
200895
+ const tryAgainAt = /try again at (\d{1,2}:\d{2}\s*[AP]M)/i.exec(ctx.errorMessage)?.[1];
200896
+ const resets = tryAgainAt ? `${ctx.account.name ?? "The provider"} says to try again at ${tryAgainAt}. ` : "";
200897
+ return renderFailure({
200898
+ headline: `Your ${ctx.account.phrase} has hit its usage limit.`,
200899
+ 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.`,
200900
+ details: providerResponse(ctx),
200901
+ links: [providerLink(ctx.repo.owner), pickModelLink(ctx.repo)]
200902
+ });
200903
+ }
200904
+ function formatTransientUpstreamBody(ctx) {
200905
+ if (/\bmodel is at capacity\b/i.test(ctx.errorMessage)) {
200906
+ return renderFailure({
200907
+ headline: `${ctx.account.name ? `${ctx.account.name}'s model` : "The model"} is at capacity right now.`,
200908
+ detail: "Re-trigger in a few minutes, or pick another model.",
200909
+ details: providerResponse(ctx),
200910
+ links: [pickModelLink(ctx.repo)]
200911
+ });
200912
+ }
200913
+ return renderFailure({
200914
+ headline: `${ctx.account.name ?? "The model provider"} had a temporary failure.`,
200915
+ detail: "Nothing on your side is at fault. Re-trigger to try again; if it keeps happening, check the provider's status page.",
200916
+ details: providerResponse(ctx)
200917
+ });
200918
+ }
200919
+ function formatProviderNoRoutableEndpoints(ctx) {
200920
+ return renderFailure({
200921
+ headline: "OpenRouter can't route this model anywhere your account allows.",
200922
+ 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.",
200923
+ details: providerResponse(ctx),
200924
+ links: [
200925
+ { label: "OpenRouter data policy", url: "https://openrouter.ai/settings/privacy" },
200926
+ pickModelLink(ctx.repo)
200927
+ ]
200928
+ });
200915
200929
  }
200916
- function formatProviderMissingCredential(input) {
200917
- return [
200918
- "**No credential reached the model provider.**",
200919
- "",
200920
- "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.",
200921
- "",
200922
- `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
200923
- "",
200924
- `\`\`\`
200925
- ${input.errorMessage}
200926
- \`\`\``
200927
- ].join("\n");
200930
+ function formatProviderMissingCredential(ctx) {
200931
+ return renderFailure({
200932
+ headline: "No credential reached the model provider.",
200933
+ 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.",
200934
+ details: providerResponse(ctx),
200935
+ links: [providerLink(ctx.repo.owner), pickModelLink(ctx.repo)]
200936
+ });
200928
200937
  }
200929
- function formatByokSetupSummary(input) {
200930
- return [
200931
- "**This repo's model isn't fully set up yet.**",
200932
- "",
200933
- input.raw,
200934
- "",
200935
- `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`
200936
- ].join("\n");
200938
+ function formatProviderModelNotFound(ctx) {
200939
+ const model = /Model not found: (\S+?)\.?(?:\s|$)/.exec(ctx.errorMessage)?.[1];
200940
+ const suggestions = /Did you mean: ([^?\n]+)\?/.exec(ctx.errorMessage)?.[1]?.split(",").map((id) => `\`${id.trim()}\``);
200941
+ const suggested = suggestions?.length ? ` OpenCode suggests ${suggestions.slice(0, -1).join(", ")}${suggestions.length > 1 ? " or " : ""}${suggestions.at(-1)}.` : "";
200942
+ return renderFailure({
200943
+ headline: model ? `\`${model}\` isn't in OpenCode's catalog.` : "This repo's model isn't in OpenCode's catalog.",
200944
+ detail: `Pick another model wherever this repo sets it: the Pullfrog console, the workflow's \`model\` input or the \`PULLFROG_MODEL\` variable.${suggested}`,
200945
+ details: { summary: "OpenCode's response", text: ctx.errorMessage },
200946
+ links: [pickModelLink(ctx.repo)]
200947
+ });
200937
200948
  }
200938
- function formatProviderModelNotFoundSummary(input) {
200939
- 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.
200940
-
200941
- \`\`\`
200942
- ${input.raw}
200943
- \`\`\``;
200949
+ function formatNoProviderAvailable(ctx) {
200950
+ return renderFailure({
200951
+ headline: "The provider won't serve this model to this account.",
200952
+ 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.",
200953
+ details: providerResponse(ctx),
200954
+ links: [pickModelLink(ctx.repo), providerLink(ctx.repo.owner)]
200955
+ });
200944
200956
  }
200945
- function formatProviderUsageLimit(input) {
200946
- return [
200947
- "**The provider's usage limit for this account has been reached.**",
200948
- "",
200949
- "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.",
200950
- "",
200951
- `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200952
- "",
200953
- `\`\`\`
200954
- ${input.raw}
200955
- \`\`\``
200956
- ].join("\n");
200957
+ function formatZenFreeTierGate(ctx) {
200958
+ return renderFailure({
200959
+ headline: "OpenCode Zen's free tier refused a request from this run.",
200960
+ 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.",
200961
+ details: { summary: "OpenCode Zen's response", text: ctx.errorMessage },
200962
+ links: [pickModelLink(ctx.repo)]
200963
+ });
200957
200964
  }
200958
- function formatProviderErrorBody(input) {
200959
- return [
200960
- "**The model provider returned an error, so this run stopped.**",
200961
- "",
200962
- "The error is below. If it names your key, plan or balance, fix that with the provider or pick another model before re-triggering.",
200963
- "",
200964
- `[Model settings \u2192](${getApiUrl()}/console/${input.owner}/${input.name}) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`,
200965
- "",
200966
- `\`\`\`
200967
- ${input.raw}
200968
- \`\`\``
200969
- ].join("\n");
200965
+ function formatUnrecognized(ctx) {
200966
+ return renderFailure({
200967
+ headline: "An unrecognized error occurred.",
200968
+ detail: "The error is below.",
200969
+ error: ctx.errorMessage,
200970
+ links: [DISCORD_LINK]
200971
+ });
200970
200972
  }
200971
- function renderRunError(input) {
200972
- const billingError = input.routerActive && isRouterKeylimitExhaustedError(input.errorMessage) ? new BillingError(input.errorMessage, { code: "router_keylimit_exhausted" }) : null;
200973
- if (billingError) {
200974
- const body = formatBillingErrorSummary(billingError, input.repo.owner);
200975
- return { summary: body, comment: body };
200976
- }
200977
- if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
200978
- return { summary: input.errorMessage, comment: input.errorMessage };
200979
- }
200980
- if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
200981
- return { summary: input.errorMessage, comment: input.errorMessage };
200973
+ function renderRunErrorBody(ctx) {
200974
+ const message2 = ctx.errorMessage;
200975
+ if (ctx.trial && isRouterKeylimitExhaustedError(message2)) {
200976
+ return buildTrialSpentError({ owner: ctx.repo.owner });
200982
200977
  }
200983
- if (input.errorMessage.includes(ROUTER_UNFUNDED_MARKER)) {
200984
- return { summary: input.errorMessage, comment: input.errorMessage };
200985
- }
200986
- if (input.errorMessage.includes(CREDENTIAL_REJECTED_MARKER) || input.errorMessage.includes(CREDENTIAL_POOL_REFUSED_MARKER)) {
200987
- return { summary: input.errorMessage, comment: input.errorMessage };
200988
- }
200989
- const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
200990
- const hangBody = isHang ? formatAgentHangBody({
200991
- diagnostic: input.agentDiagnostic,
200992
- isHang: true,
200993
- errorMessage: input.errorMessage
200994
- }) : null;
200995
- if (isProviderBillingExhausted(input.errorMessage)) {
200996
- const body = formatProviderBillingExhausted({ errorMessage: input.errorMessage });
200997
- return { summary: `### \u274C Pullfrog failed
200998
-
200999
- ${body}`, comment: body };
201000
- }
201001
- if (isProviderNoRoutableEndpoints(input.errorMessage)) {
201002
- const body = formatProviderNoRoutableEndpoints({
201003
- owner: input.repo.owner,
201004
- name: input.repo.name,
201005
- errorMessage: input.errorMessage
201006
- });
201007
- return { summary: `### \u274C Pullfrog failed
201008
-
201009
- ${body}`, comment: body };
201010
- }
201011
- if (isProviderUsageLimit(input.errorMessage)) {
201012
- const body = formatProviderUsageLimit({
201013
- owner: input.repo.owner,
201014
- name: input.repo.name,
201015
- raw: input.errorMessage
201016
- });
201017
- return { summary: `### \u274C Pullfrog failed
201018
-
201019
- ${body}`, comment: body };
201020
- }
201021
- if (isProviderMissingCredential(input.errorMessage)) {
201022
- const body = formatProviderMissingCredential({
201023
- owner: input.repo.owner,
201024
- name: input.repo.name,
201025
- errorMessage: input.errorMessage
201026
- });
201027
- return { summary: `### \u274C Pullfrog failed
201028
-
201029
- ${body}`, comment: body };
201030
- }
201031
- if (isByokSetupError(input.errorMessage)) {
201032
- const body = formatByokSetupSummary({
201033
- owner: input.repo.owner,
201034
- name: input.repo.name,
201035
- raw: input.errorMessage
201036
- });
201037
- return { summary: `### \u274C Pullfrog failed
201038
-
201039
- ${body}`, comment: body };
201040
- }
201041
- const apiKeySource = hangBody ?? input.errorMessage;
201042
- const apiKeyErrorSummary = isApiKeyAuthError(apiKeySource) ? formatApiKeyErrorSummary({
201043
- owner: input.repo.owner,
201044
- name: input.repo.name,
201045
- raw: apiKeySource
201046
- }) : null;
201047
- if (apiKeyErrorSummary) {
201048
- return { summary: apiKeyErrorSummary, comment: apiKeyErrorSummary };
201049
- }
201050
- if (isProviderModelNotFoundError(input.errorMessage)) {
201051
- const body = formatProviderModelNotFoundSummary({
201052
- owner: input.repo.owner,
201053
- name: input.repo.name,
201054
- raw: input.errorMessage
201055
- });
201056
- return { summary: body, comment: body };
200978
+ if (ctx.routerActive && isRouterKeylimitExhaustedError(message2)) {
200979
+ return formatBillingErrorSummary(
200980
+ new BillingError(message2, { code: "router_keylimit_exhausted" }),
200981
+ ctx.repo.owner
200982
+ );
201057
200983
  }
201058
- if (isNoProviderAvailableError(input.errorMessage)) {
201059
- const body = formatNoProviderAvailableSummary({
201060
- owner: input.repo.owner,
201061
- name: input.repo.name,
201062
- raw: input.errorMessage
200984
+ if ([
200985
+ MODEL_ACCESS_MARKER,
200986
+ SECRETS_UNAVAILABLE_MARKER,
200987
+ ROUTER_UNFUNDED_MARKER,
200988
+ TRIAL_SPENT_MARKER,
200989
+ CREDENTIAL_REJECTED_MARKER,
200990
+ CREDENTIAL_POOL_REFUSED_MARKER
200991
+ ].some((marker) => message2.includes(marker))) {
200992
+ return message2;
200993
+ }
200994
+ const isHang = message2.startsWith("activity timeout") || message2.startsWith("agent still pending");
200995
+ const hangBody = isHang ? formatAgentHangBody({ diagnostic: ctx.agentDiagnostic, errorMessage: message2 }) : null;
200996
+ if (isProviderBillingExhausted(message2)) return formatProviderBillingExhausted(ctx);
200997
+ if (isProviderNoRoutableEndpoints(message2)) return formatProviderNoRoutableEndpoints(ctx);
200998
+ if (isProviderUsageLimit(message2)) return formatProviderUsageLimit(ctx);
200999
+ if (isProviderMissingCredential(message2)) return formatProviderMissingCredential(ctx);
201000
+ if (isByokSetupError(message2)) {
201001
+ return renderFailure({
201002
+ headline: "This repo's model isn't fully set up yet.",
201003
+ detail: message2
201004
+ });
201005
+ }
201006
+ const apiKeySource = hangBody ?? message2;
201007
+ if (isApiKeyAuthError(apiKeySource)) {
201008
+ return formatApiKeyErrorSummary({
201009
+ owner: ctx.repo.owner,
201010
+ name: ctx.repo.name,
201011
+ raw: apiKeySource,
201012
+ model: ctx.model,
201013
+ credential: ctx.credential,
201014
+ output: hangBody ? void 0 : message2
201015
+ });
201016
+ }
201017
+ if (isProviderModelNotFoundError(message2)) return formatProviderModelNotFound(ctx);
201018
+ if (isNoProviderAvailableError(message2)) return formatNoProviderAvailable(ctx);
201019
+ if (isZenFreeTierGateError(message2)) return formatZenFreeTierGate(ctx);
201020
+ if (isContextOverflowError(message2)) {
201021
+ return renderFailure({
201022
+ headline: "This run outgrew the model's context window.",
201023
+ detail: "Pick a model with a larger context window, or split the PR into smaller ones and re-trigger.",
201024
+ links: [pickModelLink(ctx.repo)]
201025
+ });
201026
+ }
201027
+ const timeout = RUN_TIMEOUT_PATTERN.exec(message2)?.[1];
201028
+ if (timeout) {
201029
+ return renderFailure({
201030
+ headline: `This run hit its ${timeout} time limit while still working.`,
201031
+ 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.",
201032
+ links: [
201033
+ { label: "Repo settings", url: repoConsoleUrl(ctx.repo) },
201034
+ { label: "Flags", url: "https://docs.pullfrog.com/flags" }
201035
+ ]
201063
201036
  });
201064
- return { summary: `### \u274C Pullfrog failed
201065
-
201066
- ${body}`, comment: body };
201067
201037
  }
201068
- if (isZenFreeTierGateError(input.errorMessage)) {
201069
- const body = formatZenFreeTierGateSummary({
201070
- owner: input.repo.owner,
201071
- name: input.repo.name,
201072
- raw: input.errorMessage
201038
+ const stopScript = STOP_SCRIPT_PATTERN.exec(message2);
201039
+ if (stopScript) {
201040
+ return renderFailure({
201041
+ headline: "This repo's stop script failed, so the run ended without finishing.",
201042
+ detail: `It exited with code ${stopScript[1]}. Fix the script in this repo's settings, or re-trigger if the failure was temporary.`,
201043
+ details: stopScript[2]?.trim() ? { summary: "Script output", text: stopScript[2].trim() } : void 0,
201044
+ links: [{ label: "Repo settings", url: repoConsoleUrl(ctx.repo) }]
201073
201045
  });
201074
- return { summary: `### \u274C Pullfrog failed
201075
-
201076
- ${body}`, comment: body };
201077
201046
  }
201078
- if (isContextOverflowError(input.errorMessage)) {
201079
- const body = formatContextOverflowSummary({
201080
- owner: input.repo.owner,
201081
- name: input.repo.name,
201082
- raw: input.errorMessage
201047
+ if (UNSUBMITTED_REVIEW_PATTERN.test(message2)) {
201048
+ return renderFailure({
201049
+ headline: "The agent finished without posting its review.",
201050
+ detail: "It was reminded and still didn't, so the run stopped. Re-trigger to try again."
201083
201051
  });
201084
- return { summary: `### \u274C Pullfrog failed
201085
-
201086
- ${body}`, comment: body };
201087
201052
  }
201088
- if (isRunTimeoutError(input.errorMessage)) {
201089
- const body = formatRunTimeoutSummary({
201090
- owner: input.repo.owner,
201091
- name: input.repo.name,
201092
- raw: input.errorMessage
201053
+ if (isTransientUpstreamError(message2)) return formatTransientUpstreamBody(ctx);
201054
+ if (hangBody) return hangBody;
201055
+ if (message2.startsWith("provider error: ")) {
201056
+ return renderFailure({
201057
+ headline: `${ctx.account.name ?? "The model provider"} returned an error, so this run stopped.`,
201058
+ detail: "If it names your key, plan or balance, fix that with the provider or pick another model.",
201059
+ error: message2.slice("provider error: ".length),
201060
+ links: [pickModelLink(ctx.repo)]
201093
201061
  });
201094
- return { summary: `### \u274C Pullfrog failed
201095
-
201096
- ${body}`, comment: body };
201097
- }
201098
- if (isTransientUpstreamError(input.errorMessage)) {
201099
- const body = formatTransientUpstreamBody(input.errorMessage);
201100
- return { summary: `### \u274C Pullfrog failed
201101
-
201102
- ${body}`, comment: body };
201103
- }
201104
- if (hangBody) {
201105
- const explained = input.agentDiagnostic?.lastProviderError !== void 0 || input.agentDiagnostic?.sawModelOutput === false;
201106
- return {
201107
- summary: `### \u274C Pullfrog failed
201108
-
201109
- ${hangBody}`,
201110
- comment: explained ? hangBody : formatMinimalFailureComment(input.repo)
201111
- };
201112
201062
  }
201113
- if (input.errorMessage.startsWith("provider error: ")) {
201114
- const body = formatProviderErrorBody({
201115
- owner: input.repo.owner,
201116
- name: input.repo.name,
201117
- raw: input.errorMessage
201118
- });
201119
- return { summary: `### \u274C Pullfrog failed
201063
+ return formatUnrecognized(ctx);
201064
+ }
201065
+ function renderRunError(input) {
201066
+ const body = renderRunErrorBody({ ...input, account: resolveAccount(input) });
201067
+ return { summary: `### \u274C Pullfrog failed
201120
201068
 
201121
201069
  ${body}`, comment: body };
201122
- }
201123
- const genericBody = formatGenericFailure(input.errorMessage);
201124
- return {
201125
- summary: `### \u274C Pullfrog failed
201126
-
201127
- ${genericBody}`,
201128
- comment: formatMinimalFailureComment(input.repo)
201129
- };
201130
201070
  }
201131
201071
 
201132
201072
  // utils/runLifecycle.ts
@@ -201552,7 +201492,10 @@ async function finalizeSuccessRun(input) {
201552
201492
  errorMessage: input.result.error || "agent run failed",
201553
201493
  repo: input.repo,
201554
201494
  agentDiagnostic: input.toolState.agentDiagnostic,
201555
- routerActive: !!input.toolContext.payload.proxyModel
201495
+ routerActive: !!input.toolContext.payload.proxyModel,
201496
+ model: input.toolState.model,
201497
+ credential: input.toolState.credential,
201498
+ trial: input.toolState.modelClamped?.reason === "trial"
201556
201499
  }) : null;
201557
201500
  if (rendered) {
201558
201501
  await reportErrorToComment({
@@ -201713,7 +201656,7 @@ function renderTodoMarkdown(todos) {
201713
201656
  case "cancelled":
201714
201657
  return `- ~~${todo.content}~~`;
201715
201658
  case "in_progress":
201716
- return `- [ ] <img src="https://uploads.pullfrog.com/Progress%20Indicator.gif" width="11" style="visibility: visible; max-width: 100%;" /> ${todo.content}`;
201659
+ return `- [ ] <img src="${TODO_SPINNER_URL}" width="11" style="visibility: visible; max-width: 100%;" /> ${todo.content}`;
201717
201660
  case "pending":
201718
201661
  return `- [ ] ${todo.content}`;
201719
201662
  default:
@@ -202100,10 +202043,15 @@ async function main() {
202100
202043
  const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
202101
202044
  payload,
202102
202045
  configuredModel: effectiveModel,
202046
+ creditsLeftUsd: runContext.trialCreditsLeftUsd,
202103
202047
  oidcCredentials,
202104
202048
  repo: runContext.repo,
202105
202049
  toolState
202106
202050
  }) : false;
202051
+ const trialSpent = runContext.trialCreditsLeftUsd !== void 0 && runContext.trialCreditsLeftUsd <= 0;
202052
+ if (!funded && trialSpent && missingKey instanceof NoUsableCredentialError) {
202053
+ throw new Error(buildTrialSpentError({ owner: runContext.repo.owner }));
202054
+ }
202107
202055
  if (!funded) throw missingKey;
202108
202056
  agent2 = resolveAgent2({
202109
202057
  model: resolvedModel,
@@ -202434,7 +202382,10 @@ ${instructions.user}` : null,
202434
202382
  errorMessage,
202435
202383
  repo: runContext.repo,
202436
202384
  agentDiagnostic: toolState.agentDiagnostic,
202437
- routerActive: !!payload.proxyModel
202385
+ routerActive: !!payload.proxyModel,
202386
+ model: toolState.model,
202387
+ credential: toolState.credential,
202388
+ trial: toolState.modelClamped?.reason === "trial"
202438
202389
  });
202439
202390
  await writeRunErrorOutputs({ rendered, toolState });
202440
202391
  if (toolContext) {
@@ -203016,7 +202967,7 @@ function resolveTarget2(repo) {
203016
202967
  return { owner: match3[1], repo: match3[2] };
203017
202968
  }
203018
202969
  function cliVersion() {
203019
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.100");
202970
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.101");
203020
202971
  if (!parts) return "0.0.0";
203021
202972
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
203022
202973
  }
@@ -203183,7 +203134,7 @@ async function runCli8(input) {
203183
203134
  }
203184
203135
 
203185
203136
  // cli.ts
203186
- var VERSION10 = "0.1.100";
203137
+ var VERSION10 = "0.1.101";
203187
203138
  var bin = basename2(process.argv[1] || "");
203188
203139
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
203189
203140
  var rawArgs = process.argv.slice(2);