pullfrog 0.1.60 → 0.1.62

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.
@@ -52,6 +52,16 @@ export declare function hasPostRunIssues(issues: PostRunIssues): boolean;
52
52
  * cache hit ratio at a glance. Dashboards that query `WorkflowRun.inputTokens`
53
53
  * directly are seeing the full total, not the log column.
54
54
  */
55
+ /**
56
+ * Which credential actually paid for a run.
57
+ *
58
+ * `subscription` is the one that changes what a cost figure MEANS: a Claude
59
+ * Pro/Max or ChatGPT subscription moves no per-token money, so a dollar figure
60
+ * on such a run is what it WOULD have cost on the API, not spend. Without this
61
+ * recorded, `WorkflowRun.costUsd` silently mixes the two and there is no way
62
+ * after the fact to separate them.
63
+ */
64
+ export type AgentCredential = "subscription" | "api_key" | "gateway" | "bedrock" | "vertex" | "foundry";
55
65
  export interface AgentUsage {
56
66
  agent: string;
57
67
  /** full billable input: non-cached + cache read + cache write */
package/dist/cli.mjs CHANGED
@@ -107293,6 +107293,9 @@ function isProviderNoRoutableEndpoints(text) {
107293
107293
  function isOpenRouterKeyLimitExceeded(text) {
107294
107294
  return /Key limit exceeded \(total limit\)/i.test(text) || /openrouter\.ai\/[^\s"')]*\/keys\//i.test(text);
107295
107295
  }
107296
+ function isProviderMissingCredential(text) {
107297
+ return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
107298
+ }
107296
107299
  function isTransientUpstreamError(text) {
107297
107300
  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);
107298
107301
  }
@@ -107355,7 +107358,7 @@ var import_semver = __toESM(require_semver2(), 1);
107355
107358
  // package.json
107356
107359
  var package_default = {
107357
107360
  name: "pullfrog",
107358
- version: "0.1.60",
107361
+ version: "0.1.62",
107359
107362
  type: "module",
107360
107363
  bin: {
107361
107364
  pullfrog: "dist/cli.mjs",
@@ -108446,7 +108449,6 @@ function getUnsubmittedReview(toolState, expectsReviewOutput2 = toolState.hadPro
108446
108449
  }
108447
108450
  function expectsReviewOutput(ctx) {
108448
108451
  if (ctx.toolState.hadProgressComment) return true;
108449
- if (ctx.payload.progressComments) return false;
108450
108452
  return ctx.payload.event.silent !== true && ctx.payload.event.issue_number !== void 0;
108451
108453
  }
108452
108454
  function buildStopHookPrompt(failure2) {
@@ -108661,6 +108663,7 @@ async function runPostRunRetryLoop(params) {
108661
108663
  result = preResume;
108662
108664
  break;
108663
108665
  }
108666
+ if (onlySummaryStale) result = { ...result, output: preResume.output || result.output };
108664
108667
  gateResumeCount++;
108665
108668
  }
108666
108669
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
@@ -109481,6 +109484,19 @@ var claude = agent({
109481
109484
  delete env2.CLAUDE_CODE_OAUTH_TOKEN;
109482
109485
  }
109483
109486
  }
109487
+ if (env2.CLAUDE_CODE_USE_BEDROCK) {
109488
+ ctx.toolState.credential = "bedrock";
109489
+ } else if (env2.CLAUDE_CODE_USE_VERTEX) {
109490
+ ctx.toolState.credential = "vertex";
109491
+ } else if (env2.CLAUDE_CODE_USE_FOUNDRY) {
109492
+ ctx.toolState.credential = "foundry";
109493
+ } else if (env2.ANTHROPIC_AUTH_TOKEN) {
109494
+ ctx.toolState.credential = "gateway";
109495
+ } else if (env2.ANTHROPIC_API_KEY) {
109496
+ ctx.toolState.credential = "api_key";
109497
+ } else if (env2.CLAUDE_CODE_OAUTH_TOKEN) {
109498
+ ctx.toolState.credential = "subscription";
109499
+ }
109484
109500
  const effortEnvOverride = env2[CLAUDE_EFFORT_ENV]?.trim();
109485
109501
  if (effortEnvOverride) {
109486
109502
  log2.warning(
@@ -109943,6 +109959,18 @@ function toTodoWriteInput(item) {
109943
109959
  }))
109944
109960
  };
109945
109961
  }
109962
+ var CODEX_MODEL_PRICING = {
109963
+ "gpt-5.6-sol": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, output: 30 },
109964
+ "gpt-5.6-luna": { input: 0.2, cacheRead: 0.02, cacheWrite: 0.25, output: 1.2 },
109965
+ "gpt-5.6-terra": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 12 }
109966
+ };
109967
+ function codexCostUsd(params) {
109968
+ const price = params.model ? CODEX_MODEL_PRICING[params.model] : void 0;
109969
+ if (!price) return void 0;
109970
+ const fresh = Math.max(0, params.input - params.cacheRead - params.cacheWrite);
109971
+ const usd = (fresh * price.input + params.cacheRead * price.cacheRead + params.cacheWrite * price.cacheWrite + params.output * price.output) / 1e6;
109972
+ return usd > 0 ? usd : void 0;
109973
+ }
109946
109974
  async function runCodex2(params) {
109947
109975
  const startTime = performance6.now();
109948
109976
  const thinkingTimer = new ThinkingTimer();
@@ -109954,14 +109982,15 @@ async function runCodex2(params) {
109954
109982
  let turnError = null;
109955
109983
  let lastProviderError = null;
109956
109984
  let stdoutBuffer = "";
109957
- const tokens = { input: 0, cacheRead: 0, output: 0 };
109985
+ const tokens = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
109958
109986
  function buildUsage2() {
109959
109987
  if (tokens.input === 0 && tokens.output === 0) return void 0;
109960
109988
  return {
109961
109989
  agent: "codex",
109962
109990
  inputTokens: tokens.input,
109963
109991
  outputTokens: tokens.output,
109964
- cacheReadTokens: tokens.cacheRead || void 0
109992
+ cacheReadTokens: tokens.cacheRead || void 0,
109993
+ costUsd: codexCostUsd({ model: params.model, ...tokens })
109965
109994
  };
109966
109995
  }
109967
109996
  function onItem(item, phase) {
@@ -110026,6 +110055,7 @@ async function runCodex2(params) {
110026
110055
  case "turn.completed":
110027
110056
  tokens.input += event.usage.input_tokens ?? 0;
110028
110057
  tokens.cacheRead += event.usage.cached_input_tokens ?? 0;
110058
+ tokens.cacheWrite += event.usage.cache_write_input_tokens ?? 0;
110029
110059
  tokens.output += event.usage.output_tokens ?? 0;
110030
110060
  return;
110031
110061
  case "turn.failed":
@@ -110186,6 +110216,11 @@ var codex = agent({
110186
110216
  } else if (process.env.OPENAI_API_KEY) {
110187
110217
  env2.CODEX_API_KEY = process.env.OPENAI_API_KEY;
110188
110218
  }
110219
+ if (codexHomeAuth) {
110220
+ ctx.toolState.credential = "subscription";
110221
+ } else if (env2.CODEX_API_KEY) {
110222
+ ctx.toolState.credential = "api_key";
110223
+ }
110189
110224
  const securityFlags = securityOverrideFlags({
110190
110225
  ctx,
110191
110226
  sandboxMode: ctx.payload.push === "disabled" ? "read-only" : "workspace-write",
@@ -110194,7 +110229,13 @@ var codex = agent({
110194
110229
  model: resolveCodexModel(ctx),
110195
110230
  effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
110196
110231
  });
110197
- const runnerArgs = { cliPath, cwd: process.cwd(), env: env2, todoTracker: ctx.todoTracker };
110232
+ const runnerArgs = {
110233
+ cliPath,
110234
+ cwd: process.cwd(),
110235
+ env: env2,
110236
+ todoTracker: ctx.todoTracker,
110237
+ model: resolveCodexModel(ctx)
110238
+ };
110198
110239
  const initial = await runCodex2({
110199
110240
  ...runnerArgs,
110200
110241
  args: [...securityFlags, "exec", "--json", ctx.instructions.full],
@@ -115967,24 +116008,23 @@ async function installOpencodeCli(params) {
115967
116008
  var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this \u2014 if the picker is locked, this run had no Pullfrog Router funding: add a card or top up your credit.";
115968
116009
  function autoSelectModel() {
115969
116010
  const authorized2 = getAuthorizedModels();
115970
- if (authorized2.size > 0) {
115971
- const servable = (a2) => {
115972
- if (!authorized2.has(a2.resolve)) return false;
115973
- const envVars = getModelEnvVars(a2.resolve);
115974
- return envVars.length === 0 || envVars.some((name) => process.env[name]);
115975
- };
115976
- const match3 = modelAliases.find((a2) => !a2.hidden && !a2.fallback && a2.preferred && servable(a2)) ?? modelAliases.find((a2) => !a2.hidden && !a2.fallback && servable(a2));
115977
- if (match3) {
115978
- log2.info(
115979
- `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
115980
- );
115981
- log2.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
115982
- return match3.resolve;
115983
- }
116011
+ const selectable = modelAliases.filter((a2) => !a2.hidden && !a2.fallback && !a2.routing);
116012
+ const servable = selectable.filter((a2) => {
116013
+ const envVars = getModelEnvVars(a2.resolve);
116014
+ return envVars.length === 0 || envVars.some((name) => process.env[name]);
116015
+ });
116016
+ const pick4 = (list) => list.find((a2) => a2.preferred) ?? list[0];
116017
+ const match3 = pick4(servable.filter((a2) => authorized2.has(a2.resolve))) ?? pick4(servable);
116018
+ if (match3) {
115984
116019
  log2.info(
115985
- `\xBB opencode has ${authorized2.size} models but none match curated aliases \u2014 letting OpenCode auto-select`
116020
+ `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
115986
116021
  );
116022
+ log2.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
116023
+ return match3.resolve;
115987
116024
  }
116025
+ log2.info(
116026
+ `\xBB opencode has ${authorized2.size} models but none match curated aliases and none hold a credential \u2014 letting OpenCode auto-select: ${[...authorized2].join(", ")}`
116027
+ );
115988
116028
  log2.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
115989
116029
  return void 0;
115990
116030
  }
@@ -188596,8 +188636,8 @@ function validatePushDestination(repoState, branch, cwd) {
188596
188636
  throw new Error(
188597
188637
  `Push blocked: destination does not match expected repository.
188598
188638
  Expected: ${pushUrl}
188599
- Actual: ${dest.url}
188600
- Git configuration may have been tampered with.`
188639
+ Actual: remote '${dest.remoteName}' -> ${dest.url}
188640
+ Git configuration may have been tampered with, or a url.*.insteadOf rewrite is in effect.`
188601
188641
  );
188602
188642
  }
188603
188643
  return dest;
@@ -189416,7 +189456,9 @@ var STRING_KEYS = [
189416
189456
  "reviewNodeId",
189417
189457
  "planCommentNodeId",
189418
189458
  "summarySnapshot",
189419
- "model"
189459
+ "model",
189460
+ "agent",
189461
+ "credential"
189420
189462
  ];
189421
189463
  var NUMBER_KEYS = [
189422
189464
  "inputTokens",
@@ -190717,6 +190759,9 @@ async function configureRepoGit(params) {
190717
190759
  removeIncludeIfEntries(repoDir);
190718
190760
  const originUrl = `https://github.com/${params.owner}/${params.name}.git`;
190719
190761
  $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
190762
+ $("git", ["config", "--local", "--replace-all", "remote.origin.pushurl", originUrl], {
190763
+ cwd: repoDir
190764
+ });
190720
190765
  repoState.pushUrl = originUrl;
190721
190766
  $("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
190722
190767
  repoState.initialHead = captureInitialHead(repoDir);
@@ -191055,6 +191100,9 @@ async function checkoutPrBranch(pr, params) {
191055
191100
  $("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
191056
191101
  log2.debug(`\xBB updated remote '${remoteName}' for fork ${pr.headRepoFullName}`);
191057
191102
  }
191103
+ $("git", ["config", "--local", "--replace-all", `remote.${remoteName}.pushurl`, forkUrl], {
191104
+ log: false
191105
+ });
191058
191106
  $("git", ["config", `branch.${localBranch}.pushRemote`, remoteName], { log: false });
191059
191107
  $("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${pr.headRef}`], { log: false });
191060
191108
  log2.debug(`\xBB configured branch '${localBranch}' to push to '${remoteName}/${pr.headRef}'`);
@@ -196195,7 +196243,10 @@ var JsonPayload = type({
196195
196243
  // optional so a payload from an older server build (pre-`checkRun`) still parses
196196
196244
  // against a newer action across a rolling deploy.
196197
196245
  "checkRun?": type({ id: "string" }).or("undefined"),
196198
- "generateSummary?": "boolean | undefined"
196246
+ "generateSummary?": "boolean | undefined",
196247
+ // optional so a payload from a pre-canary server build still parses against a
196248
+ // newer action across a rolling deploy.
196249
+ "codexArm?": "boolean | undefined"
196199
196250
  });
196200
196251
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
196201
196252
  function isCollaborator(event) {
@@ -196324,6 +196375,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
196324
196375
  progressComment: jsonPayload?.progressComment,
196325
196376
  checkRun: jsonPayload?.checkRun,
196326
196377
  generateSummary: jsonPayload?.generateSummary,
196378
+ codexArm: jsonPayload?.codexArm,
196327
196379
  // permissions: inputs > repoSettings > fallbacks
196328
196380
  push: inputs.push ?? repoSettings.push ?? "restricted",
196329
196381
  shell: resolvedShell,
@@ -196872,7 +196924,7 @@ async function handleAgentResult(ctx) {
196872
196924
  const mode = toolState.selectedMode;
196873
196925
  const isReviewMode = mode === "Review" || mode === "IncrementalReview";
196874
196926
  const payload = ctx.toolContext.payload;
196875
- const hasCommentTarget = toolState.hadProgressComment || !payload.progressComments && payload.event.issue_number !== void 0;
196927
+ const hasCommentTarget = toolState.hadProgressComment || payload.event.issue_number !== void 0;
196876
196928
  if (!isReviewMode && !toolState.wasUpdated && hasCommentTarget && !ctx.silent) {
196877
196929
  const tracker = toolState.todoTracker;
196878
196930
  if (tracker) {
@@ -197306,6 +197358,19 @@ ${input.errorMessage}
197306
197358
  \`\`\``
197307
197359
  ].join("\n");
197308
197360
  }
197361
+ function formatProviderMissingCredential(input) {
197362
+ return [
197363
+ "**No credential reached the model provider.**",
197364
+ "",
197365
+ "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.",
197366
+ "",
197367
+ `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
197368
+ "",
197369
+ `\`\`\`
197370
+ ${input.errorMessage}
197371
+ \`\`\``
197372
+ ].join("\n");
197373
+ }
197309
197374
  function formatProviderModelNotFoundSummary(input) {
197310
197375
  return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
197311
197376
 
@@ -197351,6 +197416,16 @@ ${body}`, comment: body };
197351
197416
  });
197352
197417
  return { summary: `### \u274C Pullfrog failed
197353
197418
 
197419
+ ${body}`, comment: body };
197420
+ }
197421
+ if (isProviderMissingCredential(input.errorMessage)) {
197422
+ const body = formatProviderMissingCredential({
197423
+ owner: input.repo.owner,
197424
+ name: input.repo.name,
197425
+ errorMessage: input.errorMessage
197426
+ });
197427
+ return { summary: `### \u274C Pullfrog failed
197428
+
197354
197429
  ${body}`, comment: body };
197355
197430
  }
197356
197431
  const apiKeySource = hangBody ?? input.errorMessage;
@@ -198268,8 +198343,12 @@ async function main() {
198268
198343
  const agent2 = resolveAgent({
198269
198344
  model: resolvedModel,
198270
198345
  proxyModel: payload.proxyModel,
198271
- codexAgent: runContext.repoSettings.codexAgent
198346
+ // the account opt-in and the canary arm are both admissions to codex, so
198347
+ // they OR: an account that opted in explicitly always gets it, and the
198348
+ // canary widens the pool without ever demoting a run that already had it.
198349
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
198272
198350
  });
198351
+ toolState.agent = agent2.name;
198273
198352
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
198274
198353
  toolState.model = effectiveModel;
198275
198354
  if (!payload.proxyModel) {
@@ -198627,6 +198706,8 @@ ${instructions.user}` : null,
198627
198706
  if (toolContext) {
198628
198707
  const patch = aggregateUsage(toolState.usageEntries);
198629
198708
  if (toolState.model) patch.model = toolState.model;
198709
+ if (toolState.agent) patch.agent = toolState.agent;
198710
+ if (toolState.credential) patch.credential = toolState.credential;
198630
198711
  if (Object.keys(patch).length > 0) {
198631
198712
  await patchWorkflowRunFields(toolContext, patch);
198632
198713
  }
@@ -199184,7 +199265,7 @@ async function runCli4(input) {
199184
199265
  }
199185
199266
 
199186
199267
  // cli.ts
199187
- var VERSION10 = "0.1.60";
199268
+ var VERSION10 = "0.1.62";
199188
199269
  var bin = basename2(process.argv[1] || "");
199189
199270
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
199190
199271
  var rawArgs = process.argv.slice(2);
@@ -322,6 +322,13 @@ export interface WriteablePayload {
322
322
  } | undefined;
323
323
  /** when true, seed the PR summary tmpfile + persist edits at run end */
324
324
  generateSummary?: boolean | undefined;
325
+ /**
326
+ * the codex canary assigned this run to the EXPERIMENTAL codex harness. rolled
327
+ * once server-side at reservation and stamped on the run row, so the row and the
328
+ * run always agree on which arm this was. ORed with the account opt-in in
329
+ * `resolveAgent` — it widens who reaches codex, never narrows it.
330
+ */
331
+ codexArm?: boolean | undefined;
325
332
  }
326
333
  export type Payload = Readonly<WriteablePayload>;
327
334
  /**
package/dist/index.js CHANGED
@@ -105126,6 +105126,9 @@ function isProviderNoRoutableEndpoints(text) {
105126
105126
  function isOpenRouterKeyLimitExceeded(text) {
105127
105127
  return /Key limit exceeded \(total limit\)/i.test(text) || /openrouter\.ai\/[^\s"')]*\/keys\//i.test(text);
105128
105128
  }
105129
+ function isProviderMissingCredential(text) {
105130
+ return /API key is missing\. Pass it using the/i.test(text) || /Missing Authentication header/i.test(text);
105131
+ }
105129
105132
  function isTransientUpstreamError(text) {
105130
105133
  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);
105131
105134
  }
@@ -105188,7 +105191,7 @@ var import_semver = __toESM(require_semver2(), 1);
105188
105191
  // package.json
105189
105192
  var package_default = {
105190
105193
  name: "pullfrog",
105191
- version: "0.1.60",
105194
+ version: "0.1.62",
105192
105195
  type: "module",
105193
105196
  bin: {
105194
105197
  pullfrog: "dist/cli.mjs",
@@ -106279,7 +106282,6 @@ function getUnsubmittedReview(toolState, expectsReviewOutput2 = toolState.hadPro
106279
106282
  }
106280
106283
  function expectsReviewOutput(ctx) {
106281
106284
  if (ctx.toolState.hadProgressComment) return true;
106282
- if (ctx.payload.progressComments) return false;
106283
106285
  return ctx.payload.event.silent !== true && ctx.payload.event.issue_number !== void 0;
106284
106286
  }
106285
106287
  function buildStopHookPrompt(failure2) {
@@ -106494,6 +106496,7 @@ async function runPostRunRetryLoop(params) {
106494
106496
  result = preResume;
106495
106497
  break;
106496
106498
  }
106499
+ if (onlySummaryStale) result = { ...result, output: preResume.output || result.output };
106497
106500
  gateResumeCount++;
106498
106501
  }
106499
106502
  if (gateResumeCount > 0 && result.success && hasPostRunIssues(finalIssues)) {
@@ -107314,6 +107317,19 @@ var claude = agent({
107314
107317
  delete env2.CLAUDE_CODE_OAUTH_TOKEN;
107315
107318
  }
107316
107319
  }
107320
+ if (env2.CLAUDE_CODE_USE_BEDROCK) {
107321
+ ctx.toolState.credential = "bedrock";
107322
+ } else if (env2.CLAUDE_CODE_USE_VERTEX) {
107323
+ ctx.toolState.credential = "vertex";
107324
+ } else if (env2.CLAUDE_CODE_USE_FOUNDRY) {
107325
+ ctx.toolState.credential = "foundry";
107326
+ } else if (env2.ANTHROPIC_AUTH_TOKEN) {
107327
+ ctx.toolState.credential = "gateway";
107328
+ } else if (env2.ANTHROPIC_API_KEY) {
107329
+ ctx.toolState.credential = "api_key";
107330
+ } else if (env2.CLAUDE_CODE_OAUTH_TOKEN) {
107331
+ ctx.toolState.credential = "subscription";
107332
+ }
107317
107333
  const effortEnvOverride = env2[CLAUDE_EFFORT_ENV]?.trim();
107318
107334
  if (effortEnvOverride) {
107319
107335
  log.warning(
@@ -107823,6 +107839,18 @@ function toTodoWriteInput(item) {
107823
107839
  }))
107824
107840
  };
107825
107841
  }
107842
+ var CODEX_MODEL_PRICING = {
107843
+ "gpt-5.6-sol": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, output: 30 },
107844
+ "gpt-5.6-luna": { input: 0.2, cacheRead: 0.02, cacheWrite: 0.25, output: 1.2 },
107845
+ "gpt-5.6-terra": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 12 }
107846
+ };
107847
+ function codexCostUsd(params) {
107848
+ const price = params.model ? CODEX_MODEL_PRICING[params.model] : void 0;
107849
+ if (!price) return void 0;
107850
+ const fresh = Math.max(0, params.input - params.cacheRead - params.cacheWrite);
107851
+ const usd = (fresh * price.input + params.cacheRead * price.cacheRead + params.cacheWrite * price.cacheWrite + params.output * price.output) / 1e6;
107852
+ return usd > 0 ? usd : void 0;
107853
+ }
107826
107854
  async function runCodex(params) {
107827
107855
  const startTime = performance6.now();
107828
107856
  const thinkingTimer = new ThinkingTimer();
@@ -107834,14 +107862,15 @@ async function runCodex(params) {
107834
107862
  let turnError = null;
107835
107863
  let lastProviderError = null;
107836
107864
  let stdoutBuffer = "";
107837
- const tokens = { input: 0, cacheRead: 0, output: 0 };
107865
+ const tokens = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
107838
107866
  function buildUsage2() {
107839
107867
  if (tokens.input === 0 && tokens.output === 0) return void 0;
107840
107868
  return {
107841
107869
  agent: "codex",
107842
107870
  inputTokens: tokens.input,
107843
107871
  outputTokens: tokens.output,
107844
- cacheReadTokens: tokens.cacheRead || void 0
107872
+ cacheReadTokens: tokens.cacheRead || void 0,
107873
+ costUsd: codexCostUsd({ model: params.model, ...tokens })
107845
107874
  };
107846
107875
  }
107847
107876
  function onItem(item, phase) {
@@ -107906,6 +107935,7 @@ async function runCodex(params) {
107906
107935
  case "turn.completed":
107907
107936
  tokens.input += event.usage.input_tokens ?? 0;
107908
107937
  tokens.cacheRead += event.usage.cached_input_tokens ?? 0;
107938
+ tokens.cacheWrite += event.usage.cache_write_input_tokens ?? 0;
107909
107939
  tokens.output += event.usage.output_tokens ?? 0;
107910
107940
  return;
107911
107941
  case "turn.failed":
@@ -108066,6 +108096,11 @@ var codex = agent({
108066
108096
  } else if (process.env.OPENAI_API_KEY) {
108067
108097
  env2.CODEX_API_KEY = process.env.OPENAI_API_KEY;
108068
108098
  }
108099
+ if (codexHomeAuth) {
108100
+ ctx.toolState.credential = "subscription";
108101
+ } else if (env2.CODEX_API_KEY) {
108102
+ ctx.toolState.credential = "api_key";
108103
+ }
108069
108104
  const securityFlags = securityOverrideFlags({
108070
108105
  ctx,
108071
108106
  sandboxMode: ctx.payload.push === "disabled" ? "read-only" : "workspace-write",
@@ -108074,7 +108109,13 @@ var codex = agent({
108074
108109
  model: resolveCodexModel(ctx),
108075
108110
  effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
108076
108111
  });
108077
- const runnerArgs = { cliPath, cwd: process.cwd(), env: env2, todoTracker: ctx.todoTracker };
108112
+ const runnerArgs = {
108113
+ cliPath,
108114
+ cwd: process.cwd(),
108115
+ env: env2,
108116
+ todoTracker: ctx.todoTracker,
108117
+ model: resolveCodexModel(ctx)
108118
+ };
108078
108119
  const initial = await runCodex({
108079
108120
  ...runnerArgs,
108080
108121
  args: [...securityFlags, "exec", "--json", ctx.instructions.full],
@@ -113847,24 +113888,23 @@ async function installOpencodeCli(params) {
113847
113888
  var AUTO_SELECT_WARNING = "select a model explicitly in the Pullfrog console (https://pullfrog.com/console) to avoid this \u2014 if the picker is locked, this run had no Pullfrog Router funding: add a card or top up your credit.";
113848
113889
  function autoSelectModel() {
113849
113890
  const authorized2 = getAuthorizedModels();
113850
- if (authorized2.size > 0) {
113851
- const servable = (a) => {
113852
- if (!authorized2.has(a.resolve)) return false;
113853
- const envVars = getModelEnvVars(a.resolve);
113854
- return envVars.length === 0 || envVars.some((name) => process.env[name]);
113855
- };
113856
- const match3 = modelAliases.find((a) => !a.hidden && !a.fallback && a.preferred && servable(a)) ?? modelAliases.find((a) => !a.hidden && !a.fallback && servable(a));
113857
- if (match3) {
113858
- log.info(
113859
- `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
113860
- );
113861
- log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
113862
- return match3.resolve;
113863
- }
113891
+ const selectable = modelAliases.filter((a) => !a.hidden && !a.fallback && !a.routing);
113892
+ const servable = selectable.filter((a) => {
113893
+ const envVars = getModelEnvVars(a.resolve);
113894
+ return envVars.length === 0 || envVars.some((name) => process.env[name]);
113895
+ });
113896
+ const pick4 = (list) => list.find((a) => a.preferred) ?? list[0];
113897
+ const match3 = pick4(servable.filter((a) => authorized2.has(a.resolve))) ?? pick4(servable);
113898
+ if (match3) {
113864
113899
  log.info(
113865
- `\xBB opencode has ${authorized2.size} models but none match curated aliases \u2014 letting OpenCode auto-select`
113900
+ `\xBB model: ${match3.resolve} (auto-selected${match3.preferred ? " \u2014 preferred" : ""} curated match)`
113866
113901
  );
113902
+ log.warning(`\xBB model auto-selected. ${AUTO_SELECT_WARNING}`);
113903
+ return match3.resolve;
113867
113904
  }
113905
+ log.info(
113906
+ `\xBB opencode has ${authorized2.size} models but none match curated aliases and none hold a credential \u2014 letting OpenCode auto-select: ${[...authorized2].join(", ")}`
113907
+ );
113868
113908
  log.warning(`\xBB no model resolved. letting OpenCode auto-select. ${AUTO_SELECT_WARNING}`);
113869
113909
  return void 0;
113870
113910
  }
@@ -186476,8 +186516,8 @@ function validatePushDestination(repoState, branch, cwd) {
186476
186516
  throw new Error(
186477
186517
  `Push blocked: destination does not match expected repository.
186478
186518
  Expected: ${pushUrl}
186479
- Actual: ${dest.url}
186480
- Git configuration may have been tampered with.`
186519
+ Actual: remote '${dest.remoteName}' -> ${dest.url}
186520
+ Git configuration may have been tampered with, or a url.*.insteadOf rewrite is in effect.`
186481
186521
  );
186482
186522
  }
186483
186523
  return dest;
@@ -187296,7 +187336,9 @@ var STRING_KEYS = [
187296
187336
  "reviewNodeId",
187297
187337
  "planCommentNodeId",
187298
187338
  "summarySnapshot",
187299
- "model"
187339
+ "model",
187340
+ "agent",
187341
+ "credential"
187300
187342
  ];
187301
187343
  var NUMBER_KEYS = [
187302
187344
  "inputTokens",
@@ -188597,6 +188639,9 @@ async function configureRepoGit(params) {
188597
188639
  removeIncludeIfEntries(repoDir);
188598
188640
  const originUrl = `https://github.com/${params.owner}/${params.name}.git`;
188599
188641
  $("git", ["remote", "set-url", "origin", originUrl], { cwd: repoDir });
188642
+ $("git", ["config", "--local", "--replace-all", "remote.origin.pushurl", originUrl], {
188643
+ cwd: repoDir
188644
+ });
188600
188645
  repoState.pushUrl = originUrl;
188601
188646
  $("git", ["config", "--local", "credential.helper", ""], { cwd: repoDir });
188602
188647
  repoState.initialHead = captureInitialHead(repoDir);
@@ -188935,6 +188980,9 @@ async function checkoutPrBranch(pr, params) {
188935
188980
  $("git", ["remote", "set-url", remoteName, forkUrl], { log: false });
188936
188981
  log.debug(`\xBB updated remote '${remoteName}' for fork ${pr.headRepoFullName}`);
188937
188982
  }
188983
+ $("git", ["config", "--local", "--replace-all", `remote.${remoteName}.pushurl`, forkUrl], {
188984
+ log: false
188985
+ });
188938
188986
  $("git", ["config", `branch.${localBranch}.pushRemote`, remoteName], { log: false });
188939
188987
  $("git", ["config", `branch.${localBranch}.merge`, `refs/heads/${pr.headRef}`], { log: false });
188940
188988
  log.debug(`\xBB configured branch '${localBranch}' to push to '${remoteName}/${pr.headRef}'`);
@@ -194075,7 +194123,10 @@ var JsonPayload = type({
194075
194123
  // optional so a payload from an older server build (pre-`checkRun`) still parses
194076
194124
  // against a newer action across a rolling deploy.
194077
194125
  "checkRun?": type({ id: "string" }).or("undefined"),
194078
- "generateSummary?": "boolean | undefined"
194126
+ "generateSummary?": "boolean | undefined",
194127
+ // optional so a payload from a pre-canary server build still parses against a
194128
+ // newer action across a rolling deploy.
194129
+ "codexArm?": "boolean | undefined"
194079
194130
  });
194080
194131
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
194081
194132
  function isCollaborator(event) {
@@ -194204,6 +194255,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
194204
194255
  progressComment: jsonPayload?.progressComment,
194205
194256
  checkRun: jsonPayload?.checkRun,
194206
194257
  generateSummary: jsonPayload?.generateSummary,
194258
+ codexArm: jsonPayload?.codexArm,
194207
194259
  // permissions: inputs > repoSettings > fallbacks
194208
194260
  push: inputs.push ?? repoSettings.push ?? "restricted",
194209
194261
  shell: resolvedShell,
@@ -194752,7 +194804,7 @@ async function handleAgentResult(ctx) {
194752
194804
  const mode = toolState.selectedMode;
194753
194805
  const isReviewMode = mode === "Review" || mode === "IncrementalReview";
194754
194806
  const payload = ctx.toolContext.payload;
194755
- const hasCommentTarget = toolState.hadProgressComment || !payload.progressComments && payload.event.issue_number !== void 0;
194807
+ const hasCommentTarget = toolState.hadProgressComment || payload.event.issue_number !== void 0;
194756
194808
  if (!isReviewMode && !toolState.wasUpdated && hasCommentTarget && !ctx.silent) {
194757
194809
  const tracker = toolState.todoTracker;
194758
194810
  if (tracker) {
@@ -195186,6 +195238,19 @@ ${input.errorMessage}
195186
195238
  \`\`\``
195187
195239
  ].join("\n");
195188
195240
  }
195241
+ function formatProviderMissingCredential(input) {
195242
+ return [
195243
+ "**No credential reached the model provider.**",
195244
+ "",
195245
+ "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.",
195246
+ "",
195247
+ `[Configure model \u2192](${getApiUrl()}/console/${input.owner}/${input.name})`,
195248
+ "",
195249
+ `\`\`\`
195250
+ ${input.errorMessage}
195251
+ \`\`\``
195252
+ ].join("\n");
195253
+ }
195189
195254
  function formatProviderModelNotFoundSummary(input) {
195190
195255
  return `The configured model is no longer available in OpenCode's catalog. Pick a different model in the Pullfrog console for \`${input.owner}/${input.name}\`, or contact support if this persists.
195191
195256
 
@@ -195231,6 +195296,16 @@ ${body}`, comment: body };
195231
195296
  });
195232
195297
  return { summary: `### \u274C Pullfrog failed
195233
195298
 
195299
+ ${body}`, comment: body };
195300
+ }
195301
+ if (isProviderMissingCredential(input.errorMessage)) {
195302
+ const body = formatProviderMissingCredential({
195303
+ owner: input.repo.owner,
195304
+ name: input.repo.name,
195305
+ errorMessage: input.errorMessage
195306
+ });
195307
+ return { summary: `### \u274C Pullfrog failed
195308
+
195234
195309
  ${body}`, comment: body };
195235
195310
  }
195236
195311
  const apiKeySource = hangBody ?? input.errorMessage;
@@ -196148,8 +196223,12 @@ async function main() {
196148
196223
  const agent2 = resolveAgent({
196149
196224
  model: resolvedModel,
196150
196225
  proxyModel: payload.proxyModel,
196151
- codexAgent: runContext.repoSettings.codexAgent
196226
+ // the account opt-in and the canary arm are both admissions to codex, so
196227
+ // they OR: an account that opted in explicitly always gets it, and the
196228
+ // canary widens the pool without ever demoting a run that already had it.
196229
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
196152
196230
  });
196231
+ toolState.agent = agent2.name;
196153
196232
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
196154
196233
  toolState.model = effectiveModel;
196155
196234
  if (!payload.proxyModel) {
@@ -196507,6 +196586,8 @@ ${instructions.user}` : null,
196507
196586
  if (toolContext) {
196508
196587
  const patch = aggregateUsage(toolState.usageEntries);
196509
196588
  if (toolState.model) patch.model = toolState.model;
196589
+ if (toolState.agent) patch.agent = toolState.agent;
196590
+ if (toolState.credential) patch.credential = toolState.credential;
196510
196591
  if (Object.keys(patch).length > 0) {
196511
196592
  await patchWorkflowRunFields(toolContext, patch);
196512
196593
  }
@@ -1,4 +1,4 @@
1
- import type { AgentUsage } from "./agents/shared.ts";
1
+ import type { AgentCredential, AgentUsage } from "./agents/shared.ts";
2
2
  import type { PrepResult } from "./prep/types.ts";
3
3
  import type { AgentDiagnostic } from "./utils/agentHangReport.ts";
4
4
  import type { DiffCoverageState } from "./utils/diffCoverage.ts";
@@ -136,6 +136,8 @@ export interface ToolState {
136
136
  output?: string | undefined;
137
137
  usageEntries: AgentUsage[];
138
138
  model?: string | undefined;
139
+ agent?: string | undefined;
140
+ credential?: AgentCredential | undefined;
139
141
  modelFallback?: {
140
142
  from: string;
141
143
  } | undefined;
@@ -10,7 +10,7 @@ import type { ToolContext } from "../mcp/server.ts";
10
10
  * don't parse the audit-only `payload`.
11
11
  * Keep in sync with `STRING_FIELDS` in `app/api/workflow-run/[runId]/route.ts`.
12
12
  */
13
- declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "planCommentNodeId", "summarySnapshot", "model"];
13
+ declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "planCommentNodeId", "summarySnapshot", "model", "agent", "credential"];
14
14
  /**
15
15
  * Number-valued usage fields — aggregated across all agent calls and PATCHed
16
16
  * once at end-of-run. Token counts are Int4 on the DB side (ample for any
@@ -28,6 +28,7 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
28
28
  id: string;
29
29
  } | undefined;
30
30
  generateSummary?: boolean | undefined;
31
+ codexArm?: boolean | undefined;
31
32
  }, {}>;
32
33
  export declare const Inputs: import("arktype/internal/variants/object.ts").ObjectType<{
33
34
  prompt?: string | undefined;
@@ -75,6 +76,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
75
76
  id: string;
76
77
  } | undefined;
77
78
  generateSummary: boolean | undefined;
79
+ codexArm: boolean | undefined;
78
80
  push: import("../external.ts").PushPermission;
79
81
  shell: import("../external.ts").ShellPermission;
80
82
  runStatusCheck: boolean;
@@ -50,6 +50,12 @@ export declare function isProviderNoRoutableEndpoints(text: string): boolean;
50
50
  * …/settings/credits`) carries no `/keys/` path and correctly stays out.
51
51
  */
52
52
  export declare function isOpenRouterKeyLimitExceeded(text: string): boolean;
53
+ /**
54
+ * the provider was reached with NO credential at all — the AI SDK's shared `loadApiKey`
55
+ * refusal, so it is provider-agnostic. NOT `isApiKeyAuthError`, which is a key we hold
56
+ * being rejected: "rotate your key" names a credential that does not exist here.
57
+ */
58
+ export declare function isProviderMissingCredential(text: string): boolean;
53
59
  /**
54
60
  * The upstream is having a moment: Anthropic's `API Error: 529 Overloaded`,
55
61
  * OpenRouter's `provider_unavailable` / `timeout`, OpenCode Zen's `Streaming
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",