pullfrog 0.1.67 → 0.1.69

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
@@ -110208,6 +110208,7 @@ import { performance as performance5 } from "node:perf_hooks";
110208
110208
 
110209
110209
  // effort.ts
110210
110210
  var DEFAULT_EFFORT_POSITION = 0.5;
110211
+ var SUBSIDY_RUNG = "high";
110211
110212
  var EFFORT_ALIASES = {
110212
110213
  low: 0,
110213
110214
  medium: 0.25,
@@ -110611,6 +110612,15 @@ var providers = {
110611
110612
  // the model does cost nothing.
110612
110613
  isFree: true
110613
110614
  },
110615
+ // Zen meters Fable like any other model, so this route reaches it without
110616
+ // the Anthropic access grant `anthropic/claude-fable` still needs.
110617
+ "claude-fable": {
110618
+ displayName: "Claude Fable",
110619
+ resolve: "opencode/claude-fable-5-1",
110620
+ effort: ["low", "medium", "high", "xhigh", "max"],
110621
+ openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
110622
+ subagentModel: "claude-sonnet"
110623
+ },
110614
110624
  "claude-opus": {
110615
110625
  displayName: "Claude Opus",
110616
110626
  resolve: "opencode/claude-opus-5",
@@ -110724,6 +110734,17 @@ var providers = {
110724
110734
  effort: ["minimal", "low", "medium", "high"],
110725
110735
  openRouterResolve: "openrouter/google/gemini-3.6-flash"
110726
110736
  },
110737
+ // Zen serves K3, but rule 7 can only move a mirror when its upstream moves,
110738
+ // so the K3 generation could never reach this block by bumping `kimi-k2`.
110739
+ "kimi-k3": {
110740
+ displayName: "Kimi K3",
110741
+ resolve: "opencode/kimi-k3",
110742
+ // Zen publishes a single rung for K3 where OpenRouter publishes three.
110743
+ effort: ["max"],
110744
+ openRouterEffort: ["low", "high", "max"],
110745
+ openRouterResolve: "openrouter/moonshotai/kimi-k3",
110746
+ subagentModel: "kimi-k2"
110747
+ },
110727
110748
  "kimi-k2": {
110728
110749
  displayName: "Kimi K2",
110729
110750
  // k2.7-code is UNDEPLOYED on Zen — still listed in /zen/v1/models, but
@@ -110735,25 +110756,57 @@ var providers = {
110735
110756
  resolve: "opencode/kimi-k2.6",
110736
110757
  openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
110737
110758
  },
110759
+ // M3 is its own line beside M2, not a bump of it — same reason `kimi-k3`
110760
+ // sits beside `kimi-k2` rather than replacing it.
110761
+ "minimax-m3": {
110762
+ displayName: "MiniMax M3",
110763
+ resolve: "opencode/minimax-m3",
110764
+ openRouterResolve: "openrouter/minimax/minimax-m3"
110765
+ },
110738
110766
  // slug pins the m2 line for DB stability; resolve tracks the current m2.7.
110739
110767
  "minimax-m2.5": {
110740
110768
  displayName: "MiniMax M2",
110741
110769
  resolve: "opencode/minimax-m2.7",
110742
110770
  openRouterResolve: "openrouter/minimax/minimax-m2.7"
110743
110771
  },
110772
+ // Z.ai and xAI reach Zen subscribers only here — before this, `opencode-go`
110773
+ // was the catalog's only GLM route, and Grok had no Zen route at all.
110774
+ glm: {
110775
+ displayName: "GLM",
110776
+ resolve: "opencode/glm-5.3",
110777
+ effort: ["low", "high", "max"],
110778
+ openRouterResolve: "openrouter/z-ai/glm-5.3"
110779
+ },
110780
+ grok: {
110781
+ displayName: "Grok",
110782
+ resolve: "opencode/grok-4.6",
110783
+ effort: ["low", "medium", "high", "xhigh"],
110784
+ openRouterResolve: "openrouter/x-ai/grok-4.6"
110785
+ },
110744
110786
  "gpt-5-nano": {
110745
110787
  displayName: "GPT Nano",
110746
110788
  resolve: "opencode/gpt-5-nano",
110747
110789
  effort: ["minimal", "low", "medium", "high"],
110748
110790
  openRouterResolve: "openrouter/openai/gpt-5-nano"
110749
110791
  },
110792
+ // Zen's live free MiMo, and the second free row in a menu that big-pickle
110793
+ // was alone in since `mimo-v2-pro-free` lost its model.
110794
+ mimo: {
110795
+ displayName: "MiMo",
110796
+ resolve: "opencode/mimo-v2.5-free",
110797
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
110798
+ // see the big-pickle note above (#1077).
110799
+ isFree: true
110800
+ },
110750
110801
  "mimo-v2-pro-free": {
110751
110802
  displayName: "MiMo V2 Pro",
110752
110803
  resolve: "opencode/mimo-v2-pro-free",
110753
110804
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
110754
110805
  // see the big-pickle note above (#1077).
110755
110806
  isFree: true,
110756
- fallback: "opencode/big-pickle"
110807
+ // the model id Zen dropped; land a stored MiMo pick back on MiMo rather
110808
+ // than on the unrelated model it had to settle for while none was live.
110809
+ fallback: "opencode/mimo"
110757
110810
  },
110758
110811
  "minimax-m2.5-free": {
110759
110812
  displayName: "MiniMax M2",
@@ -110769,10 +110822,11 @@ var providers = {
110769
110822
  // OpenCode Go is a separate $10/mo subscription from Zen, served on its own
110770
110823
  // base URL (`https://opencode.ai/zen/go/v1`) but authenticated with the SAME
110771
110824
  // `OPENCODE_API_KEY`. it carries the open-weight coding models plus a couple
110772
- // of frontier ones, and 14 of the ids below are served ONLY here — Zen's
110773
- // `/v1/models` does not list glm-5.3*, qwen3.7/3.8-*, mimo-*, longcat-2.0,
110774
- // hy3 or muse-spark. so for a Go subscriber this provider is not a duplicate
110775
- // route to Zen, it is the only route to most of what they pay for.
110825
+ // of frontier ones, six of which are served ONLY here — Zen's `/v1/models`
110826
+ // does not list qwen3.7/3.8-*, mimo-v2.5-pro, longcat-2.0 or hy3. (it listed
110827
+ // no glm-5.3* either until 2026-09, which is why `opencode/glm` trailed on
110828
+ // 5.2.) so for a Go subscriber this provider is not a duplicate route to Zen,
110829
+ // it is the only route to a large part of what they pay for.
110776
110830
  // like `opencode` and `openrouter` this is a ROUTER, not a vendor: slugs and
110777
110831
  // display names mirror the upstream brand tier, and the picker groups them
110778
110832
  // under the upstream vendor.
@@ -110841,13 +110895,25 @@ var providers = {
110841
110895
  // `high` the Go route does not.
110842
110896
  effort: ["low", "medium", "xhigh"],
110843
110897
  openRouterEffort: ["minimal", "low", "medium", "high", "xhigh"],
110844
- openRouterResolve: "openrouter/qwen/qwen3.8-max"
110898
+ // OpenRouter retired the rolling `qwen3.8-max` for a dated snapshot, so
110899
+ // this side has to pin one; the Go route still carries the rolling id.
110900
+ openRouterResolve: "openrouter/qwen/qwen3.8-max-0902"
110845
110901
  },
110846
110902
  "qwen-plus": {
110847
110903
  displayName: "Qwen Plus",
110848
110904
  resolve: "opencode-go/qwen3.7-plus",
110849
110905
  openRouterResolve: "openrouter/qwen/qwen3.7-plus"
110850
110906
  },
110907
+ // the cheap rung Alibaba added under Plus (0.15/0.47 against 0.5/3), and
110908
+ // the only model either OpenCode plan has added since this block was written.
110909
+ "qwen-flash": {
110910
+ displayName: "Qwen Flash",
110911
+ resolve: "opencode-go/qwen3.8-flash",
110912
+ effort: ["low", "medium", "xhigh"],
110913
+ // the Go route publishes rungs where OpenRouter publishes none.
110914
+ openRouterEffort: [],
110915
+ openRouterResolve: "openrouter/qwen/qwen3.8-flash"
110916
+ },
110851
110917
  // MiniMax — parity with opencode/* and openrouter/*; the m2 slug pins the
110852
110918
  // line for DB stability while the resolve tracks the current m2.7.
110853
110919
  "minimax-m3": {
@@ -111972,6 +112038,7 @@ async function installFromNpmTarball(params) {
111972
112038
  // utils/providerErrors.ts
111973
112039
  var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
111974
112040
  var PROVIDER_NO_ENDPOINTS_LABEL = "provider no routable endpoints";
112041
+ var PROVIDER_USAGE_LIMIT_LABEL = "provider usage limit reached";
111975
112042
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
111976
112043
  var ANTHROPIC_SPEND_CAP_PATTERN = /reached your specified API usage limits\.?(?:\s*You will regain access on ([^"\n]+?)\.)?/i;
111977
112044
  function findAnthropicSpendCap(text) {
@@ -112017,6 +112084,14 @@ var PROVIDER_ERROR_PATTERNS = [
112017
112084
  // the wallet-shaped patterns above reach, and its 400 sits nowhere near a
112018
112085
  // `status:` key either — so the whole message was dropped on the floor (#1208).
112019
112086
  { regex: ANTHROPIC_SPEND_CAP_PATTERN, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
112087
+ // OpenCode Zen / Codex-subscription cap: `AI_APICallError: The usage limit has
112088
+ // been reached`. Its noun is "usage limit" with no "rate", no status code and
112089
+ // no wallet wording, so every pattern here missed it and `lastProviderError`
112090
+ // stayed unset — which suppresses nothing, so the run was reported as "The
112091
+ // model produced no output at all … usually transient; re-running often
112092
+ // succeeds" while the same log held nine of these. Measured on a live account
112093
+ // whose ChatGPT subscription was capped.
112094
+ { regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
112020
112095
  // the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
112021
112096
  // run those mean the PULLFROG wallet is empty and `renderRunError` returns a
112022
112097
  // `BillingError` before ever reaching this list; on a BYOK run the very same
@@ -112147,7 +112222,7 @@ function ossEffortFloor(ctx) {
112147
112222
  const alias = resolveRunAlias(ctx);
112148
112223
  const published = alias?.openRouterEffort ?? alias?.effort;
112149
112224
  if (!published) return 0;
112150
- return rungPosition({ rung: "high", published }) ?? 0;
112225
+ return rungPosition({ rung: SUBSIDY_RUNG, published }) ?? 0;
112151
112226
  }
112152
112227
  function resolveRunEffort(ctx) {
112153
112228
  const configured = ctx.payload.effort !== void 0;
@@ -112179,7 +112254,7 @@ var import_semver = __toESM(require_semver2(), 1);
112179
112254
  // package.json
112180
112255
  var package_default = {
112181
112256
  name: "pullfrog",
112182
- version: "0.1.67",
112257
+ version: "0.1.69",
112183
112258
  type: "module",
112184
112259
  bin: {
112185
112260
  pullfrog: "dist/cli.mjs",
@@ -112986,6 +113061,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
112986
113061
  - record what was done (or why nothing was done)
112987
113062
 
112988
113063
  5. Quality check:
113064
+ - re-call \`${t2("get_review_comments")}\` with \`fresh: true\` before committing: reviewers keep commenting while you work, and any thread it now returns that you have not addressed is in scope for this run.
112989
113065
  - test changes, then review the diff before committing \u2014 verify only intended changes are present, no debug artifacts remain, no fix turned out to be bloat in context (revert any that did), and the changes are clean enough that a senior engineer would approve without hesitation
112990
113066
  - ${commitStep}
112991
113067
 
@@ -112995,7 +113071,7 @@ For simple, well-defined tasks, skip the plan phase and go straight to build.`
112995
113071
  - **once the fix is live on the remote**, for each thread you acted on:
112996
113072
  - reply ONCE via \`${t2("reply_to_review_comment")}\`. The \`comment_id\` parameter takes the root comment's numeric \`id=\` (from the first \`comment author=...\` tag in the \`${t2("get_review_comments")}\` output) \u2014 NOT the \`thread=\` value; that's a separate GraphQL ID used by resolve. The runtime dedupes identical bodies within a session.
112997
113073
  - **immediately** call \`${t2("resolve_review_thread")}\` with that thread's \`thread=\` value as \`thread_id\`. Resolve every thread where you (a) made the requested code change in full \u2014 partial fixes leave the thread open \u2014 OR (b) replied with a substantive answer the user explicitly asked for. Do NOT resolve threads where you pushed back on the request and the disagreement is unresolved; leave those open for the human to mediate.
112998
- - call \`${t2("report_progress")}\` with a brief summary`
113074
+ - call \`${t2("report_progress")}\` with a brief summary. If your last \`${t2("get_review_comments")}\` turned up threads you did not address, name them there \u2014 a review arriving this late is suppressed as a duplicate and nothing else will pick it up.`
112999
113075
  },
113000
113076
  // Review and IncrementalReview route the minimum reviewfrog specialists
113001
113077
  // needed to cover unresolved, disposition-changing hypotheses. Most runs
@@ -194790,6 +194866,9 @@ function formatModelLabel(params) {
194790
194866
  if (params.fallbackFrom) {
194791
194867
  return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
194792
194868
  }
194869
+ if (params.clamped?.reason === "trial") {
194870
+ return `${base} (free trial)`;
194871
+ }
194793
194872
  if (params.clamped) {
194794
194873
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
194795
194874
  return params.clamped.reason === "card" ? `${base} (${target} needs a [card on file](https://docs.pullfrog.com/models))` : `${base} (${target} needs a [provider key](https://docs.pullfrog.com/models) \u2014 no Router support yet)`;
@@ -194799,6 +194878,15 @@ function formatModelLabel(params) {
194799
194878
  }
194800
194879
  return base;
194801
194880
  }
194881
+ function buildTrialDisclosure(owner) {
194882
+ const consoleUrl2 = owner ? `https://pullfrog.com/console/${owner}` : "https://pullfrog.com/console";
194883
+ return [
194884
+ "---",
194885
+ "",
194886
+ "> [!IMPORTANT]",
194887
+ `> **This run used the free trial model.** \`DeepSeek Flash\` is fast and cheap \u2014 expect lighter work than a frontier model. [Connect a subscription or API key \u2192](${consoleUrl2})`
194888
+ ].join("\n");
194889
+ }
194802
194890
  function buildPullfrogFooter(params) {
194803
194891
  const parts = [];
194804
194892
  if (params.shaPinned) {
@@ -194831,10 +194919,13 @@ function buildPullfrogFooter(params) {
194831
194919
  );
194832
194920
  }
194833
194921
  const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
194922
+ const disclosure = params.clamped?.reason === "trial" ? `${buildTrialDisclosure(params.owner)}
194923
+
194924
+ ` : "";
194834
194925
  return `
194835
194926
 
194836
194927
  ${PULLFROG_DIVIDER}
194837
- <sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
194928
+ ${disclosure}<sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
194838
194929
  }
194839
194930
  function stripExistingFooter(body) {
194840
194931
  const dividerIndex = body.indexOf(PULLFROG_DIVIDER);
@@ -194988,7 +195079,8 @@ function buildCommentFooter(ctx, customParts) {
194988
195079
  clamped: ctx.toolState.modelClamped,
194989
195080
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
194990
195081
  shaPinned: ctx.toolState.shaPinned,
194991
- oss: ctx.oss
195082
+ oss: ctx.oss,
195083
+ owner: ctx.repo.owner
194992
195084
  });
194993
195085
  }
194994
195086
  function buildImplementPlanLink(ctx, issueNumber, commentId) {
@@ -195987,7 +196079,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
195987
196079
  clamped: ctx.toolState.modelClamped,
195988
196080
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
195989
196081
  shaPinned: ctx.toolState.shaPinned,
195990
- oss: ctx.oss
196082
+ oss: ctx.oss,
196083
+ owner: ctx.repo.owner
195991
196084
  });
195992
196085
  return await ctx.octokit.rest.pulls.submitReview({
195993
196086
  owner: params.owner,
@@ -198748,7 +198841,8 @@ function buildPrBodyWithFooter(ctx, body) {
198748
198841
  clamped: ctx.toolState.modelClamped,
198749
198842
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
198750
198843
  shaPinned: ctx.toolState.shaPinned,
198751
- oss: ctx.oss
198844
+ oss: ctx.oss,
198845
+ owner: ctx.repo.owner
198752
198846
  });
198753
198847
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
198754
198848
  return `${bodyWithoutFooter}${footer}`;
@@ -199083,7 +199177,10 @@ function extractFromFilePatches(hunks, startLine, endLine, side) {
199083
199177
  }
199084
199178
  var GetReviewComments = type({
199085
199179
  pull_number: type.number.describe("The pull request number"),
199086
- review_id: type.number.describe("The review ID to get comments for")
199180
+ review_id: type.number.describe("The review ID to get comments for"),
199181
+ "fresh?": type.boolean.describe(
199182
+ "Bypass the 60s thread cache. Use for the re-read before committing, so comments that landed while you worked are actually seen; leave unset otherwise."
199183
+ )
199087
199184
  });
199088
199185
  function hasThumbsUpFrom(comment, username) {
199089
199186
  if (!comment.reactionGroups) return false;
@@ -199117,7 +199214,7 @@ function formatReviewThreads(threadBlocks, header) {
199117
199214
  }
199118
199215
  const lines = [];
199119
199216
  lines.push(
199120
- `# Review Threads (${threadBlocks.length}) for PR #${header.pullNumber} - Review ${header.reviewId} by ${header.reviewer}`
199217
+ `# Open Threads (${threadBlocks.length}) on PR #${header.pullNumber} - dispatched by Review ${header.reviewId} from ${header.reviewer}`
199121
199218
  );
199122
199219
  lines.push("");
199123
199220
  if (threadBlocks.length > 0) {
@@ -199258,11 +199355,27 @@ async function getReviewThreads(input2) {
199258
199355
  if (!thread?.comments?.nodes) return false;
199259
199356
  return thread.comments.nodes.some((c2) => c2?.pullRequestReview?.databaseId === input2.reviewId);
199260
199357
  });
199261
- if (!input2.approvedBy) {
199262
- return threadsForReview;
199358
+ if (input2.approvedBy) {
199359
+ const username = input2.approvedBy;
199360
+ return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
199263
199361
  }
199264
- const username = input2.approvedBy;
199265
- return threadsForReview.filter((thread) => threadHasThumbsUpFrom(thread, username));
199362
+ if (!input2.addressScope) return threadsForReview;
199363
+ return [...threadsForReview, ...concurrentThreads(allThreads, threadsForReview, input2)];
199364
+ }
199365
+ function concurrentThreads(allThreads, threadsForReview, input2) {
199366
+ const alreadyIncluded = new Set(threadsForReview.map((thread) => thread.id));
199367
+ return allThreads.filter((thread) => {
199368
+ if (!thread?.comments?.nodes || alreadyIncluded.has(thread.id)) return false;
199369
+ if (thread.isResolved) return false;
199370
+ const comments = thread.comments.nodes.filter((c2) => c2 !== null);
199371
+ if (comments.length === 0) return false;
199372
+ if (comments.some((c2) => isPullfrog(c2.author?.login))) return false;
199373
+ return input2.addressScope === "all" || comments.some((c2) => mentionsPullfrog(c2.body));
199374
+ });
199375
+ }
199376
+ function mentionsPullfrog(body) {
199377
+ if (!body) return false;
199378
+ return body.split(/\r?\n/).filter((line) => !line.trim().startsWith(">")).join("\n").toLowerCase().includes("@pullfrog");
199266
199379
  }
199267
199380
  function formatReviewData(input2) {
199268
199381
  const rawReviewBody = input2.review.body;
@@ -199359,6 +199472,7 @@ function GetReviewCommentsTool(ctx) {
199359
199472
  parameters: GetReviewComments,
199360
199473
  execute: execute(async (params) => {
199361
199474
  const approvedBy = ctx.payload.event.trigger === "fix_review" && ctx.payload.event.approved_only ? ctx.payload.triggerer : void 0;
199475
+ if (params.fresh) invalidateReviewThreadCache(ctx.repo);
199362
199476
  const result = await getReviewData({
199363
199477
  octokit: ctx.octokit,
199364
199478
  owner: ctx.repo.owner,
@@ -199366,6 +199480,7 @@ function GetReviewCommentsTool(ctx) {
199366
199480
  pullNumber: params.pull_number,
199367
199481
  reviewId: params.review_id,
199368
199482
  approvedBy,
199483
+ addressScope: ctx.payload.event.address_scope,
199369
199484
  tmpdir: ctx.tmpdir,
199370
199485
  githubToken: ctx.githubInstallationToken
199371
199486
  });
@@ -199396,7 +199511,7 @@ function GetReviewCommentsTool(ctx) {
199396
199511
  threadCount: threadBlocks.length,
199397
199512
  commentsPath,
199398
199513
  toc: formatted.toc,
199399
- instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}). the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
199514
+ instructions: `the file at commentsPath contains ${threadBlocks.length} review threads with full conversation history. comments marked with * are from the target review (${params.review_id}); any other thread is open feedback on this PR that is also yours to address, including comments that landed after this run started. the TOC shows each thread's file:line and the line number where it appears in the file. to read a specific thread, use: grep -A 50 "^## <file:line>" ${commentsPath} (replace <file:line> with the path from the TOC, e.g. "^## action/utils/foo.ts:42"). address each thread in order, working through one file at a time.`
199400
199515
  };
199401
199516
  })
199402
199517
  });
@@ -200055,10 +200170,12 @@ function isByokSetupError(text) {
200055
200170
  var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
200056
200171
  var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
200057
200172
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
200058
- function buildKeyError(params) {
200059
- if (params.secretsUnavailable) return buildSecretsUnavailableError(params);
200060
- if (params.routerUnfunded) return buildRouterUnfundedError(params);
200061
- return buildMissingApiKeyError(params);
200173
+ var NoUsableCredentialError = class extends Error {
200174
+ };
200175
+ function throwKeyError(params) {
200176
+ if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError(params));
200177
+ if (params.routerUnfunded) throw new NoUsableCredentialError(buildRouterUnfundedError(params));
200178
+ throw new NoUsableCredentialError(buildMissingApiKeyError(params));
200062
200179
  }
200063
200180
  function buildRouterUnfundedError(params) {
200064
200181
  const billingUrl = `${getApiUrl()}/console/${params.owner}#billing`;
@@ -200257,57 +200374,51 @@ function validateAgentApiKey(params) {
200257
200374
  validateVertexSetup({ owner: params.owner, name: params.name });
200258
200375
  return;
200259
200376
  }
200260
- validateBedrockSetup({ owner: params.owner, name: params.name });
200261
- return;
200377
+ if (process.env[BEDROCK_MODEL_ID_ENV]?.trim() === params.model) {
200378
+ validateBedrockSetup({ owner: params.owner, name: params.name });
200379
+ return;
200380
+ }
200262
200381
  }
200263
200382
  if (params.agent.name === "opencode") {
200264
200383
  if (params.authorized.has(params.model)) return;
200265
200384
  const reason = getModelsFailure();
200266
200385
  if (reason) throw new Error(reason);
200267
200386
  if (getModelEnvVars(params.model).some(hasEnvVar2)) return;
200268
- throw new Error(
200269
- buildKeyError({
200270
- owner: params.owner,
200271
- name: params.name,
200272
- model: params.model,
200273
- secretsUnavailable: params.secretsUnavailable,
200274
- routerUnfunded: params.routerUnfunded
200275
- })
200276
- );
200277
- }
200278
- if (hasSingleProviderAuth(params.agent.name)) return;
200279
- throw new Error(
200280
- buildKeyError({
200387
+ throwKeyError({
200281
200388
  owner: params.owner,
200282
200389
  name: params.name,
200283
200390
  model: params.model,
200284
200391
  secretsUnavailable: params.secretsUnavailable,
200285
200392
  routerUnfunded: params.routerUnfunded
200286
- })
200287
- );
200393
+ });
200394
+ }
200395
+ if (hasSingleProviderAuth(params.agent.name)) return;
200396
+ throwKeyError({
200397
+ owner: params.owner,
200398
+ name: params.name,
200399
+ model: params.model,
200400
+ secretsUnavailable: params.secretsUnavailable,
200401
+ routerUnfunded: params.routerUnfunded
200402
+ });
200288
200403
  }
200289
200404
  if (params.agent.name === "opencode") {
200290
200405
  if ([...params.authorized].some(modelHasRuntimeAuth)) return;
200291
200406
  const reason = getModelsFailure();
200292
200407
  if (reason) throw new Error(reason);
200293
- throw new Error(
200294
- buildKeyError({
200295
- owner: params.owner,
200296
- name: params.name,
200297
- secretsUnavailable: params.secretsUnavailable,
200298
- routerUnfunded: params.routerUnfunded
200299
- })
200300
- );
200301
- }
200302
- if (hasSingleProviderAuth(params.agent.name)) return;
200303
- throw new Error(
200304
- buildKeyError({
200408
+ throwKeyError({
200305
200409
  owner: params.owner,
200306
200410
  name: params.name,
200307
200411
  secretsUnavailable: params.secretsUnavailable,
200308
200412
  routerUnfunded: params.routerUnfunded
200309
- })
200310
- );
200413
+ });
200414
+ }
200415
+ if (hasSingleProviderAuth(params.agent.name)) return;
200416
+ throwKeyError({
200417
+ owner: params.owner,
200418
+ name: params.name,
200419
+ secretsUnavailable: params.secretsUnavailable,
200420
+ routerUnfunded: params.routerUnfunded
200421
+ });
200311
200422
  }
200312
200423
  function isApiKeyAuthError(text) {
200313
200424
  if (!text) return false;
@@ -200432,9 +200543,9 @@ function formatApiKeyErrorSummary(params) {
200432
200543
  ].join("\n");
200433
200544
  }
200434
200545
  return [
200435
- `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
200546
+ `**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.`,
200436
200547
  "",
200437
- `[Update 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)`
200548
+ `[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)`
200438
200549
  ].join("\n");
200439
200550
  }
200440
200551
 
@@ -201660,6 +201771,9 @@ var DebugInput = type.enumerated("disabled", "enabled");
201660
201771
  var JsonPayload = type({
201661
201772
  "~pullfrog": "true",
201662
201773
  version: "string",
201774
+ // the server's run type, forwarded verbatim to run-context. optional so a
201775
+ // payload from an older server build still parses against a newer action.
201776
+ "type?": "string | undefined",
201663
201777
  "model?": "string | undefined",
201664
201778
  "modelExplicit?": "boolean | undefined",
201665
201779
  "effort?": "number | string | undefined",
@@ -202145,7 +202259,8 @@ ${ctx.error}` : ctx.error;
202145
202259
  clamped: ctx.toolState.modelClamped,
202146
202260
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
202147
202261
  shaPinned: ctx.toolState.shaPinned,
202148
- oss: ctx.toolState.oss
202262
+ oss: ctx.toolState.oss,
202263
+ owner: repoContext.owner
202149
202264
  });
202150
202265
  const body = `${formattedError}${footer}`;
202151
202266
  const comment = ctx.toolState.progressComment;
@@ -202228,7 +202343,7 @@ async function mintProxyKey(ctx) {
202228
202343
  }
202229
202344
  }
202230
202345
  async function buildProxyTokenHeaders(ctx) {
202231
- const fundingSource = ctx.oss ? "oss" : "router";
202346
+ const fundingSource = ctx.fundingSource;
202232
202347
  if (ctx.oidcCredentials) {
202233
202348
  const creds = ctx.oidcCredentials;
202234
202349
  const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
@@ -202260,7 +202375,7 @@ async function resolveProxyModel(ctx) {
202260
202375
  const key = await mintProxyKey({
202261
202376
  oidcCredentials: ctx.oidcCredentials,
202262
202377
  repo: ctx.repo,
202263
- oss: ctx.oss
202378
+ fundingSource: ctx.oss ? "oss" : "router"
202264
202379
  });
202265
202380
  if (!key) return;
202266
202381
  process.env.OPENROUTER_API_KEY = key;
@@ -202332,6 +202447,26 @@ async function runProxyResolution(ctx) {
202332
202447
  throw error62;
202333
202448
  }
202334
202449
  }
202450
+ async function resolveTrialFallback(ctx) {
202451
+ if (process.env.PULLFROG_MODEL?.trim()) return false;
202452
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) return false;
202453
+ const key = await mintProxyKey({
202454
+ oidcCredentials: ctx.oidcCredentials,
202455
+ repo: ctx.repo,
202456
+ fundingSource: "trial"
202457
+ });
202458
+ if (!key) return false;
202459
+ process.env.OPENROUTER_API_KEY = key;
202460
+ core9.setSecret(key);
202461
+ ctx.payload.proxyModel = DEFAULT_PROXY_MODEL;
202462
+ ctx.toolState.model = DEFAULT_PROXY_MODEL;
202463
+ ctx.toolState.modelClamped = {
202464
+ from: ctx.configuredModel ?? DEFAULT_PROXY_MODEL,
202465
+ reason: "trial"
202466
+ };
202467
+ log2.info(`\xBB proxy: trial \u2192 ${DEFAULT_PROXY_MODEL}`);
202468
+ return true;
202469
+ }
202335
202470
 
202336
202471
  // utils/prSummary.ts
202337
202472
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
@@ -202509,8 +202644,9 @@ async function fetchRunContext(params) {
202509
202644
  if (params.oidcToken) {
202510
202645
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
202511
202646
  }
202647
+ const query = params.runType ? `?type=${encodeURIComponent(params.runType)}` : "";
202512
202648
  const response = await apiFetch({
202513
- path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
202649
+ path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context${query}`,
202514
202650
  headers,
202515
202651
  signal: controller.signal
202516
202652
  });
@@ -202547,7 +202683,8 @@ async function fetchRunContext(params) {
202547
202683
  proxyModel: data.proxyModel,
202548
202684
  dbSecrets: data.dbSecrets,
202549
202685
  secretsUnavailable: data.secretsUnavailable,
202550
- routerUnfunded: data.routerUnfunded
202686
+ routerUnfunded: data.routerUnfunded,
202687
+ trialFallback: data.trialFallback
202551
202688
  };
202552
202689
  } catch {
202553
202690
  clearTimeout(timeoutId);
@@ -202588,7 +202725,7 @@ async function resolveRunContextData(params) {
202588
202725
  retries: [100, 500],
202589
202726
  bail: (error62) => !isTransientOctokitError(error62)
202590
202727
  })(),
202591
- fetchRunContext({ token: params.token, repoContext, oidcToken })
202728
+ fetchRunContext({ token: params.token, repoContext, oidcToken, runType: params.runType })
202592
202729
  ]);
202593
202730
  return {
202594
202731
  repo: {
@@ -202606,7 +202743,8 @@ async function resolveRunContextData(params) {
202606
202743
  // a failed mint on a runner that should have been able to mint is the same
202607
202744
  // outcome as the server-side failure: the run never sees stored secrets.
202608
202745
  secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0,
202609
- routerUnfunded: runContext.routerUnfunded
202746
+ routerUnfunded: runContext.routerUnfunded,
202747
+ trialFallback: runContext.trialFallback
202610
202748
  };
202611
202749
  }
202612
202750
 
@@ -203686,7 +203824,11 @@ async function main() {
203686
203824
  resolveGit();
203687
203825
  const jobToken = getJobToken();
203688
203826
  const initialOctokit = createOctokit(jobToken);
203689
- const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
203827
+ const runContext = await resolveRunContextData({
203828
+ octokit: initialOctokit,
203829
+ token: jobToken,
203830
+ runType: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.type
203831
+ });
203690
203832
  timer.checkpoint("runContextData");
203691
203833
  const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
203692
203834
  const toolState = initToolState({
@@ -203867,7 +204009,7 @@ async function main() {
203867
204009
  }
203868
204010
  const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
203869
204011
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
203870
- const agent2 = resolveAgent({
204012
+ let agent2 = resolveAgent({
203871
204013
  model: resolvedModel,
203872
204014
  proxyModel: payload.proxyModel,
203873
204015
  // the account opt-in and the canary arm are both admissions to codex, so
@@ -203879,15 +204021,32 @@ async function main() {
203879
204021
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
203880
204022
  toolState.model = effectiveModel;
203881
204023
  if (!payload.proxyModel) {
203882
- validateAgentApiKey({
203883
- agent: agent2,
203884
- model: effectiveModel,
203885
- authorized: getAuthorizedModels(),
203886
- owner: runContext.repo.owner,
203887
- name: runContext.repo.name,
203888
- secretsUnavailable: runContext.secretsUnavailable,
203889
- routerUnfunded: runContext.routerUnfunded
203890
- });
204024
+ try {
204025
+ validateAgentApiKey({
204026
+ agent: agent2,
204027
+ model: effectiveModel,
204028
+ authorized: getAuthorizedModels(),
204029
+ owner: runContext.repo.owner,
204030
+ name: runContext.repo.name,
204031
+ secretsUnavailable: runContext.secretsUnavailable,
204032
+ routerUnfunded: runContext.routerUnfunded
204033
+ });
204034
+ } catch (missingKey) {
204035
+ const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
204036
+ payload,
204037
+ configuredModel: effectiveModel,
204038
+ oidcCredentials,
204039
+ repo: runContext.repo,
204040
+ toolState
204041
+ }) : false;
204042
+ if (!funded) throw missingKey;
204043
+ agent2 = resolveAgent({
204044
+ model: resolvedModel,
204045
+ proxyModel: payload.proxyModel,
204046
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
204047
+ });
204048
+ toolState.agent = agent2.name;
204049
+ }
203891
204050
  }
203892
204051
  await setupGit({
203893
204052
  gitToken: tokenRef.gitToken,
@@ -204797,7 +204956,7 @@ function resolveTarget(repo) {
204797
204956
  return { owner: match3[1], repo: match3[2] };
204798
204957
  }
204799
204958
  function cliVersion() {
204800
- const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.67");
204959
+ const parts = /^(\d+)\.(\d+)\.(\d+)$/.exec("0.1.69");
204801
204960
  if (!parts) return "0.0.0";
204802
204961
  return `${Number(parts[1])}.${Number(parts[2])}.${Number(parts[3])}`;
204803
204962
  }
@@ -204964,7 +205123,7 @@ async function runCli5(input2) {
204964
205123
  }
204965
205124
 
204966
205125
  // cli.ts
204967
- var VERSION10 = "0.1.67";
205126
+ var VERSION10 = "0.1.69";
204968
205127
  var bin = basename2(process.argv[1] || "");
204969
205128
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
204970
205129
  var rawArgs = process.argv.slice(2);
package/dist/effort.d.ts CHANGED
@@ -32,6 +32,16 @@ export type EffortPosition = number;
32
32
  * ran at before this setting existed.
33
33
  */
34
34
  export declare const DEFAULT_EFFORT_POSITION: EffortPosition;
35
+ /**
36
+ * the rung a Pullfrog-subsidised OSS run is pinned to, whatever the repo or a
37
+ * per-kind-of-work row has stored — see `ossEffortFloor`, which spends it, and
38
+ * the console's Model card, which stops offering the control because of it.
39
+ *
40
+ * shared rather than written twice: the runtime pins the run and the console
41
+ * says so, and a console that names a different rung than the one that runs is
42
+ * the same class of bug as the Router opt-out the two surfaces disagreed on.
43
+ */
44
+ export declare const SUBSIDY_RUNG = "high";
35
45
  /**
36
46
  * names we accept on user-facing surfaces (`--effort=`, the action input),
37
47
  * mapped to fixed points on the axis. these are a typing convenience, NOT an