pullfrog 0.1.104 → 0.1.106

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.
@@ -375,8 +375,9 @@ export interface WriteablePayload {
375
375
  export type Payload = Readonly<WriteablePayload>;
376
376
  /**
377
377
  * Whether `find_similar_issues` is registered for this run: an entitled account,
378
- * on an issue rather than a PR. Shared by the MCP registration and the prompt so
379
- * the duplicate-detection instructions can never describe an absent tool.
378
+ * on an issue or a run with no target (`pullfrog run "enrich issue #42"`), never a PR.
379
+ * Shared by the MCP registration and the prompt so the duplicate-detection
380
+ * instructions can never describe an absent tool.
380
381
  */
381
382
  export declare function hasSimilarIssues(params: {
382
383
  repoIntelligence: boolean;
package/dist/index.js CHANGED
@@ -109939,9 +109939,34 @@ var AZURE_USE_CHAT_COMPLETIONS_ENV = "AZURE_USE_CHAT_COMPLETIONS";
109939
109939
  var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
109940
109940
  var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
109941
109941
  var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
109942
- var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
109943
109942
  var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
109944
109943
  var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
109944
+ function isOpenAICompatibleSetName(name) {
109945
+ return name !== "byok" && name.length <= 32 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
109946
+ }
109947
+ function openAICompatibleSetName(slug2) {
109948
+ const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
109949
+ if (!slug2.startsWith(prefix)) return void 0;
109950
+ const name = slug2.slice(prefix.length);
109951
+ return isOpenAICompatibleSetName(name) ? name : void 0;
109952
+ }
109953
+ var OPENAI_COMPATIBLE_FIELDS = [
109954
+ "baseUrl",
109955
+ "apiKey",
109956
+ "model",
109957
+ "context",
109958
+ "maxOutput"
109959
+ ];
109960
+ function openAICompatibleEnv(name) {
109961
+ const prefix = `OPENAI_COMPATIBLE_${name ? `${name.toUpperCase().replaceAll("-", "_")}_` : ""}`;
109962
+ return {
109963
+ baseUrl: `${prefix}BASE_URL`,
109964
+ apiKey: `${prefix}API_KEY`,
109965
+ model: `${prefix}MODEL`,
109966
+ context: `${prefix}CONTEXT`,
109967
+ maxOutput: `${prefix}MAX_OUTPUT`
109968
+ };
109969
+ }
109945
109970
  var PROVIDER_GATEWAY_URL_ENV = {
109946
109971
  anthropic: "ANTHROPIC_BASE_URL",
109947
109972
  openai: "OPENAI_BASE_URL"
@@ -109986,7 +110011,7 @@ function findDanglingPromptToolRefs(params) {
109986
110011
  return [...new Set(extractMcpToolRefs(params.agentId, params.prompt))].filter((name) => !registered.has(name)).sort();
109987
110012
  }
109988
110013
  function hasSimilarIssues(params) {
109989
- return params.repoIntelligence && !!params.event.issue_number && !params.event.is_pr;
110014
+ return params.repoIntelligence && !params.event.is_pr;
109990
110015
  }
109991
110016
 
109992
110017
  // utils/activity.ts
@@ -111814,7 +111839,7 @@ import { chmodSync, createWriteStream, existsSync as existsSync3, mkdirSync } fr
111814
111839
  import { join } from "node:path";
111815
111840
  import { pipeline } from "node:stream/promises";
111816
111841
  function resolveNpm() {
111817
- const npmCli = process.env.npm_execpath;
111842
+ const npmCli = process.env.NPM_EXECPATH;
111818
111843
  if (npmCli?.endsWith("npm-cli.js")) return { command: process.execPath, args: [npmCli] };
111819
111844
  return { command: "npm", args: [] };
111820
111845
  }
@@ -112115,7 +112140,11 @@ function resolveRunEffort(ctx) {
112115
112140
  const configured = ctx.payload.effort !== void 0;
112116
112141
  const position = ctx.payload.effort ?? DEFAULT_EFFORT_POSITION;
112117
112142
  const alias = resolveRunAlias(ctx);
112118
- if (!alias) return { position, configured, rung: void 0, alias: void 0 };
112143
+ if (!alias) {
112144
+ const declarable = configured && !ctx.resolvedModel?.startsWith(`${AZURE_PROVIDER}/`);
112145
+ const rung = declarable && ctx.declaredVariants ? resolveRung({ position, published: ctx.declaredVariants }) : void 0;
112146
+ return { position, configured, rung, alias: void 0, declarable };
112147
+ }
112119
112148
  return {
112120
112149
  position,
112121
112150
  configured,
@@ -112124,7 +112153,8 @@ function resolveRunEffort(ctx) {
112124
112153
  position,
112125
112154
  useOpenRouter: !!ctx.payload.proxyModel
112126
112155
  }),
112127
- alias
112156
+ alias,
112157
+ declarable: false
112128
112158
  };
112129
112159
  }
112130
112160
 
@@ -112141,7 +112171,7 @@ var import_semver = __toESM(require_semver2(), 1);
112141
112171
  // package.json
112142
112172
  var package_default = {
112143
112173
  name: "pullfrog",
112144
- version: "0.1.104",
112174
+ version: "0.1.106",
112145
112175
  cliContractVersion: "1",
112146
112176
  type: "module",
112147
112177
  bin: {
@@ -124270,6 +124300,17 @@ function parseModel2(value2) {
124270
124300
  if (slash <= 0) return void 0;
124271
124301
  return { providerID: value2.slice(0, slash), modelID: value2.slice(slash + 1) };
124272
124302
  }
124303
+ async function resolveDeclaredRung(ctx, params) {
124304
+ const resp = await params.client.config.providers();
124305
+ const provider2 = resp.data?.providers.find((p) => p.id === params.model.providerID);
124306
+ const variants = provider2?.models[params.model.modelID]?.variants ?? {};
124307
+ const name = `${params.model.providerID}/${params.model.modelID}`;
124308
+ const rung = resolveRunEffort({ ...ctx, declaredVariants: Object.keys(variants) }).rung;
124309
+ log.info(
124310
+ `\xBB effort: ${rung ? `${rung} (declared by opencode for ${name})` : `not applied \u2014 opencode declares no variants for ${name}`}`
124311
+ );
124312
+ return rung;
124313
+ }
124273
124314
  function bootOpencodeServer(params) {
124274
124315
  const logLevel = isDebugEnabled() ? "INFO" : "ERROR";
124275
124316
  if (logLevel !== "ERROR") log.info(`\xBB opencode log level: ${logLevel} (debug run)`);
@@ -124924,6 +124965,11 @@ var opencode = agent({
124924
124965
  log.info(`\xBB opencode session: ${sessionID}`);
124925
124966
  const labeler = new SessionLabeler();
124926
124967
  labeler.labelFor(sessionID);
124968
+ const sdkModel = parseModel2(model);
124969
+ const variant = effort.declarable && sdkModel ? await resolveDeclaredRung(
124970
+ { ...ctx, resolvedModel: rawModel },
124971
+ { client: client2, model: sdkModel }
124972
+ ) : effort.rung;
124927
124973
  const runnerCtx = {
124928
124974
  client: client2,
124929
124975
  sessionID,
@@ -124932,7 +124978,7 @@ var opencode = agent({
124932
124978
  labeler,
124933
124979
  toolState: ctx.toolState,
124934
124980
  // `rawModel` folds in the auto-select pick — see the opencode.ts twin.
124935
- variant: effort.rung,
124981
+ variant,
124936
124982
  todoTracker: ctx.todoTracker,
124937
124983
  onActivityTimeout: ctx.onActivityTimeout,
124938
124984
  onTurnRecovered: ctx.onTurnRecovered,
@@ -124994,7 +125040,6 @@ var opencode = agent({
124994
125040
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
124995
125041
  abortTurn: () => turnController.abort()
124996
125042
  });
124997
- const sdkModel = parseModel2(model);
124998
125043
  let salvagesLeft = 1;
124999
125044
  const runTurn = async (text) => {
125000
125045
  const attempt = (prompt) => {
@@ -190132,14 +190177,15 @@ function computeIncrementalDiff(params) {
190132
190177
  const tips = [`origin/${params.baseBranch}`, ...params.formerBaseTips];
190133
190178
  const oldBase = versionBase({ sha: params.beforeSha, tips });
190134
190179
  const newBase = versionBase({ sha: params.headSha, tips });
190180
+ const replayed = oldBase === newBase ? null : replay({ sha: params.beforeSha, from: oldBase, onto: newBase });
190135
190181
  const raw2 = $(
190136
190182
  "sh",
190137
190183
  [
190138
190184
  "-c",
190139
190185
  'git range-diff --no-color --creation-factor=1000 "$1..$(git commit-tree "$2^{tree}" -p "$1" -m x)" "$3..$(git commit-tree "$4^{tree}" -p "$3" -m x)"',
190140
190186
  "--",
190141
- oldBase,
190142
- params.beforeSha,
190187
+ replayed ? newBase : oldBase,
190188
+ replayed ?? params.beforeSha,
190143
190189
  newBase,
190144
190190
  params.headSha
190145
190191
  ],
@@ -190174,6 +190220,22 @@ function versionBase(params) {
190174
190220
  }
190175
190221
  return best.sha;
190176
190222
  }
190223
+ function replay(params) {
190224
+ try {
190225
+ return git([
190226
+ "merge-tree",
190227
+ "--write-tree",
190228
+ `--merge-base=${params.from}`,
190229
+ params.onto,
190230
+ params.sha
190231
+ ]);
190232
+ } catch {
190233
+ log.info(
190234
+ `\xBB incremental diff: ${params.sha.slice(0, 7)} does not replay onto the new base; comparing each on its own`
190235
+ );
190236
+ return null;
190237
+ }
190238
+ }
190177
190239
  function isDiffPrefix(ch) {
190178
190240
  return ch === " " || ch === "+" || ch === "-";
190179
190241
  }
@@ -196384,13 +196446,16 @@ function buildImplementPlanLink(ctx, issueNumber, commentId) {
196384
196446
  const apiUrl = getApiUrl();
196385
196447
  return `[Implement plan \u2794](${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=implement&comment_id=${commentId})`;
196386
196448
  }
196387
- function buildEnrichmentLinks(ctx) {
196449
+ function runIssueNumber(ctx) {
196450
+ return ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196451
+ }
196452
+ function buildEnrichmentLinks(ctx, issueNumber) {
196388
196453
  const event = ctx.payload.event;
196389
- if (event.trigger !== "issues_opened" || event.is_pr || ctx.payload.prompt || ctx.toolState.selectedMode === "Build") {
196390
- return [];
196391
- }
196454
+ const mode = ctx.toolState.selectedMode;
196455
+ const enriching = mode === "Enrich" || event.trigger === "issues_opened" && !ctx.payload.prompt && mode !== "Build";
196456
+ if (issueNumber === void 0 || event.is_pr || !enriching) return [];
196392
196457
  return [
196393
- `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${event.issue_number}?action=build)`
196458
+ `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=build)`
196394
196459
  ];
196395
196460
  }
196396
196461
  function addFooter(ctx, body) {
@@ -196426,10 +196491,11 @@ function CreateCommentTool(ctx) {
196426
196491
  });
196427
196492
  ctx.toolState.wasUpdated = true;
196428
196493
  log.info(`\xBB created comment ${result.data.id}`);
196429
- if (issueNumber === ctx.payload.event.issue_number) {
196494
+ const ownTarget = issueNumber === runIssueNumber(ctx);
196495
+ if (ownTarget) {
196430
196496
  ctx.toolState.standaloneCommentId = result.data.id;
196431
196497
  }
196432
- const enrichmentLinks = issueNumber === ctx.payload.event.issue_number ? buildEnrichmentLinks(ctx) : [];
196498
+ const enrichmentLinks = ownTarget ? buildEnrichmentLinks(ctx, issueNumber) : [];
196433
196499
  if (commentType === "Plan" || enrichmentLinks.length > 0) {
196434
196500
  if (commentType === "Plan" && result.data.node_id) {
196435
196501
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -196509,9 +196575,9 @@ async function reportProgress(ctx, params) {
196509
196575
  if (params.liveProgress && !ctx.payload.progressComments) {
196510
196576
  return { body, action: "skipped" };
196511
196577
  }
196512
- const issueNumber = ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196578
+ const issueNumber = runIssueNumber(ctx);
196513
196579
  const isPlanMode = ctx.toolState.selectedMode === "Plan";
196514
- const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx);
196580
+ const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx, issueNumber);
196515
196581
  const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
196516
196582
  if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === void 0) {
196517
196583
  log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
@@ -197429,11 +197495,19 @@ import { execFileSync as execFileSync6, execSync as execSync2 } from "node:child
197429
197495
  import { mkdtempSync as mkdtempSync2, readdirSync, realpathSync as realpathSync2, rmSync as rmSync2, unlinkSync as unlinkSync2 } from "node:fs";
197430
197496
  import { tmpdir as tmpdir2 } from "node:os";
197431
197497
  import { join as join15 } from "node:path";
197498
+ var TEMP_DIR_STATE = "temp_dir";
197432
197499
  function createTempDirectory() {
197433
197500
  const path4 = mkdtempSync2(join15(tmpdir2(), "pullfrog-"));
197434
197501
  process.env.PULLFROG_TEMP_DIR = path4;
197435
197502
  log.info(`\xBB created temp dir at ${path4}`);
197436
- return { path: path4, [Symbol.dispose]: () => removeTempDirectory(path4) };
197503
+ const tracked = process.env.RUNNER_ENVIRONMENT !== "github-hosted";
197504
+ if (tracked) saveSecretState(TEMP_DIR_STATE, path4);
197505
+ return {
197506
+ path: path4,
197507
+ [Symbol.dispose]: () => {
197508
+ if (removeTempDirectory(path4) && tracked) saveSecretState(TEMP_DIR_STATE, "");
197509
+ }
197510
+ };
197437
197511
  }
197438
197512
  function removeTempDirectory(path4) {
197439
197513
  try {
@@ -197444,10 +197518,12 @@ function removeTempDirectory(path4) {
197444
197518
  rmSync2(path4, { recursive: true, force: true });
197445
197519
  }
197446
197520
  log.debug(`\xBB removed temp dir at ${path4}`);
197521
+ return true;
197447
197522
  } catch (err) {
197448
197523
  log.warning(
197449
197524
  `\xBB could not remove temp dir at ${path4}: ${err instanceof Error ? err.message : String(err)}`
197450
197525
  );
197526
+ return false;
197451
197527
  }
197452
197528
  }
197453
197529
  function wipeRunnerLeakSurface() {
@@ -199522,7 +199598,7 @@ function GetIssueCommentsTool(ctx) {
199522
199598
  description: "Get all comments for a GitHub issue. Returns all comments including the issue body and all subsequent discussion comments. Example: `get_issue_comments({ issue_number: 1234 })`.",
199523
199599
  parameters: GetIssueComments,
199524
199600
  execute: execute(async ({ issue_number }) => {
199525
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199601
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199526
199602
  const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
199527
199603
  owner: ctx.repo.owner,
199528
199604
  repo: ctx.repo.name,
@@ -199560,7 +199636,7 @@ function GetIssueEventsTool(ctx) {
199560
199636
  description: "Get timeline events for a GitHub issue that aren't reflected in the current state. Returns cross-references to other issues/PRs and commit references. Note: current labels, assignees, state, and milestone are already available via get_issue.",
199561
199637
  parameters: GetIssueEvents,
199562
199638
  execute: execute(async ({ issue_number }) => {
199563
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199639
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199564
199640
  const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
199565
199641
  owner: ctx.repo.owner,
199566
199642
  repo: ctx.repo.name,
@@ -199644,7 +199720,7 @@ function IssueInfoTool(ctx) {
199644
199720
  tmpdir: ctx.tmpdir,
199645
199721
  githubToken: ctx.githubInstallationToken
199646
199722
  });
199647
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199723
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199648
199724
  const hints = [];
199649
199725
  if (data.comments > 0) {
199650
199726
  hints.push("use get_issue_comments to retrieve all comments for this issue");
@@ -201112,8 +201188,11 @@ function SimilarIssuesTool(ctx) {
201112
201188
  mutates: true,
201113
201189
  parameters: SimilarIssues,
201114
201190
  execute: execute(async (input2) => {
201115
- if (ctx.payload.event.is_pr || ctx.payload.event.issue_number !== input2.issue_number) {
201116
- throw new Error("find_similar_issues is limited to the issue that triggered this run");
201191
+ const repoState = primaryRepoState(ctx.toolState);
201192
+ repoState.issueNumber ??= input2.issue_number;
201193
+ const subject = ctx.payload.event.issue_number ?? repoState.issueNumber;
201194
+ if (ctx.payload.event.is_pr || subject !== input2.issue_number) {
201195
+ throw new Error(`find_similar_issues is limited to this run's issue, #${subject}`);
201117
201196
  }
201118
201197
  const response = await apiFetch({
201119
201198
  path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issues/${input2.issue_number}/similar`,
@@ -201395,7 +201474,7 @@ function buildCommonTools(ctx, outputSchema) {
201395
201474
  if (hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event })) {
201396
201475
  tools.push(SimilarIssuesTool(ctx));
201397
201476
  }
201398
- if (ctx.payload.event.issue_number !== void 0 && !ctx.payload.event.is_pr) {
201477
+ if (!ctx.payload.event.is_pr) {
201399
201478
  tools.push(SearchIssuesTool(ctx));
201400
201479
  }
201401
201480
  const isStandalone = ctx.payload.event.trigger === "unknown";
@@ -201793,11 +201872,11 @@ function buildOpenAICompatibleSetupError(params) {
201793
201872
  missing: params.missing,
201794
201873
  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)}):`,
201795
201874
  env: [
201796
- `${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1`,
201797
- `${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}`,
201798
- `${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>`,
201799
- `${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"`,
201800
- `${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"`
201875
+ `${params.env.baseUrl}: https://your-endpoint.example.com/v1`,
201876
+ `${params.env.apiKey}: \${{ secrets.${params.env.apiKey} }}`,
201877
+ `${params.env.model}: <model-id>`,
201878
+ `${params.env.context}: "128000"`,
201879
+ `${params.env.maxOutput}: "16384"`
201801
201880
  ],
201802
201881
  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.",
201803
201882
  guide: "https://docs.pullfrog.com/openai-compatible"
@@ -201831,17 +201910,16 @@ function hasPositiveNumberEnvVar(name) {
201831
201910
  return Number(process.env[name]) > 0;
201832
201911
  }
201833
201912
  function validateOpenAICompatibleSetup(params) {
201913
+ const env2 = openAICompatibleEnv(params.set);
201834
201914
  const missing = [];
201835
- if (!hasEnvVar2(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
201836
- if (!hasEnvVar2(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
201837
- if (!hasEnvVar2(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
201838
- if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
201839
- missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
201840
- if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
201841
- missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
201915
+ if (!hasEnvVar2(env2.baseUrl)) missing.push(env2.baseUrl);
201916
+ if (!hasEnvVar2(env2.apiKey)) missing.push(env2.apiKey);
201917
+ if (!hasEnvVar2(env2.model)) missing.push(env2.model);
201918
+ if (!hasPositiveNumberEnvVar(env2.context)) missing.push(env2.context);
201919
+ if (!hasPositiveNumberEnvVar(env2.maxOutput)) missing.push(env2.maxOutput);
201842
201920
  if (missing.length > 0) {
201843
201921
  throw new Error(
201844
- buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
201922
+ buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing, env: env2 })
201845
201923
  );
201846
201924
  }
201847
201925
  }
@@ -201897,7 +201975,11 @@ function validateAgentApiKey(params) {
201897
201975
  return;
201898
201976
  }
201899
201977
  if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
201900
- validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
201978
+ validateOpenAICompatibleSetup({
201979
+ owner: params.owner,
201980
+ name: params.name,
201981
+ set: params.openAICompatibleSet
201982
+ });
201901
201983
  return;
201902
201984
  }
201903
201985
  if (params.model.startsWith(`${AZURE_PROVIDER}/`)) {
@@ -202211,17 +202293,37 @@ function resolveSlug(slug2) {
202211
202293
  }
202212
202294
  return `${AZURE_PROVIDER}/${deployment}`;
202213
202295
  }
202214
- if (alias?.routing === "openai-compatible") {
202215
- const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
202296
+ const set2 = heldOpenAICompatibleSet(slug2);
202297
+ if (alias?.routing === "openai-compatible" || set2) {
202298
+ const modelEnv = openAICompatibleEnv(set2).model;
202299
+ const modelId = process.env[modelEnv]?.trim();
202216
202300
  if (!modelId) {
202217
202301
  throw new Error(
202218
- `\`${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)`
202302
+ `\`${modelEnv}\` ${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)`
202219
202303
  );
202220
202304
  }
202221
202305
  return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
202222
202306
  }
202223
202307
  return resolveCliModel(slug2);
202224
202308
  }
202309
+ function heldOpenAICompatibleSet(slug2) {
202310
+ const set2 = openAICompatibleSetName(slug2);
202311
+ return set2 && Object.values(openAICompatibleEnv(set2)).some(hasEnvVar3) ? set2 : void 0;
202312
+ }
202313
+ function activateOpenAICompatibleSet(ctx) {
202314
+ const slug2 = process.env.PULLFROG_MODEL?.trim() || ctx.slug?.trim();
202315
+ const set2 = slug2 ? heldOpenAICompatibleSet(slug2) : void 0;
202316
+ if (!set2) return void 0;
202317
+ const from = openAICompatibleEnv(set2);
202318
+ const to = openAICompatibleEnv();
202319
+ for (const field of OPENAI_COMPATIBLE_FIELDS) {
202320
+ const value2 = process.env[from[field]];
202321
+ if (value2 === void 0) delete process.env[to[field]];
202322
+ else process.env[to[field]] = value2;
202323
+ }
202324
+ log.info(`\xBB using the OpenAI-compatible endpoint "${set2}"`);
202325
+ return set2;
202326
+ }
202225
202327
  function resolveModel(ctx) {
202226
202328
  const envModel = process.env.PULLFROG_MODEL?.trim();
202227
202329
  if (envModel) {
@@ -203264,7 +203366,7 @@ Rules:
203264
203366
  ### GitHub
203265
203367
 
203266
203368
  Use MCP tools from ${pullfrogMcpName} for all GitHub operations. Never use the \`gh\` CLI \u2014 it is not authenticated and will fail. The MCP tools handle authentication and enforce permissions.
203267
- ${hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event }) ? `
203369
+ ${ctx.payload.event.issue_number !== void 0 && hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event }) ? `
203268
203370
  #### Duplicate detection (enabled for this repository)
203269
203371
 
203270
203372
  Call \`${t("find_similar_issues")}\` for #${ctx.payload.event.issue_number} before planning. If it duplicates an existing issue, link that instead of producing a plan; never close or label on similarity alone.
@@ -205143,6 +205245,9 @@ function resolveModelForLog(ctx) {
205143
205245
  function resolveEffortForLog(ctx) {
205144
205246
  const effort = resolveRunEffort(ctx);
205145
205247
  if (!ctx.resolvedModel && !ctx.payload.proxyModel) return "pending \u2014 model not chosen yet";
205248
+ if (effort.declarable && ctx.agentName === "opencode") {
205249
+ return "pending \u2014 reading the variants opencode declares";
205250
+ }
205146
205251
  if (!effort.alias) return "not applied \u2014 model not recognized";
205147
205252
  if (!effort.rung) return "n/a (model has no effort control)";
205148
205253
  return effort.configured ? effort.rung : `${effort.rung} (default)`;
@@ -205161,9 +205266,7 @@ function logRunStartup(ctx) {
205161
205266
  log.info(
205162
205267
  `\xBB model: ${resolveModelForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205163
205268
  );
205164
- log.info(
205165
- `\xBB effort: ${resolveEffortForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205166
- );
205269
+ log.info(`\xBB effort: ${resolveEffortForLog(ctx)}`);
205167
205270
  const routing = ctx.payload.routing;
205168
205271
  if (routing) {
205169
205272
  log.info(
@@ -205434,6 +205537,7 @@ async function main() {
205434
205537
  const count = Object.keys(runContext.dbSecrets).length;
205435
205538
  if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
205436
205539
  }
205540
+ const openAICompatibleSet = activateOpenAICompatibleSet({ slug: payload.model });
205437
205541
  await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
205438
205542
  installCodexAuth();
205439
205543
  installXaiAuth();
@@ -205582,7 +205686,8 @@ async function main() {
205582
205686
  owner: runContext.repo.owner,
205583
205687
  name: runContext.repo.name,
205584
205688
  secretsUnavailable: runContext.secretsUnavailable,
205585
- routerUnfunded: runContext.routerUnfunded
205689
+ routerUnfunded: runContext.routerUnfunded,
205690
+ openAICompatibleSet
205586
205691
  });
205587
205692
  } catch (missingKey) {
205588
205693
  const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
@@ -7,6 +7,7 @@ export type { ConfigField, ConfigScope, ConfigValue } from "../configuration.ts"
7
7
  export { configEntrySchema, configFields, configResponseSchema, configValueSchema, } from "../configuration.ts";
8
8
  export type { AuthorPermission, AutoTier, EffortPosition, ModelAlias, ModelProvider, Payload, PayloadEvent, PayloadRouting, ProviderConfig, PushPermission, RouterSource, RouterTier, ShellPermission, ToolPermission, WriteablePayload, XrepoConfig, } from "../external.ts";
9
9
  export { AUTO_EFFICIENT, AUTO_INTELLIGENT, AUTO_ROUTER, autoLadder, autoRoutedSlug, DEFAULT_EFFORT_POSITION, DEFAULT_PROXY_MODEL, defaultAutoTier, EFFORT_ALIASES, getAutoSelectHintModel, getModelEffortLevels, getModelEnvVars, getModelManagedCredentials, getModelProvider, getProviderDisplayName, isAutoRouted, isAutoSlug, isAutoTier, isCardGatedModel, isEffortPosition, isOssAllowedModel, isRouterTier, ladderRungs, modelAliases, modelHasStoredAuth, modelIdLabel, OSS_MODEL_ALLOWLIST, OSS_RECOMMENDED_MODEL, offeredRungs, ossFundedModelNames, PROVIDER_GATEWAY_URL_ENV, parseEffortPosition, parseModel, providers, pullfrogMcpName, ROUTED_PROVIDERS, ROUTER_LADDER, ROUTER_TIERS, resolveAutoTier, resolveCliModel, resolveDisplayAlias, resolveModelRung, resolveModelSlug, resolveOpenRouterModel, resolvePickModel, resolveRoutedModel, resolveRung, routerProxyOptOut, rungLabel, rungPosition, SUBSIDY_RUNG, TRIAL_FUNDED_USD_LIMIT, } from "../external.ts";
10
+ export { isOpenAICompatibleSetName, openAICompatibleEnv, openAICompatibleSetAlias, openAICompatibleSetName, openAICompatibleSetOf, openAICompatibleSets, } from "../models.ts";
10
11
  export type { Mode } from "../modes.ts";
11
12
  export { modes } from "../modes.ts";
12
13
  export { type CommercialRefusal, commercialPaywallBody } from "../utils/billingErrors.ts";
package/dist/internal.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "pullfrog",
11
- version: "0.1.104",
11
+ version: "0.1.106",
12
12
  cliContractVersion: "1",
13
13
  type: "module",
14
14
  bin: {
@@ -20079,7 +20079,7 @@ var repoConfigFields = [
20079
20079
  key: "address-reviews.enabled",
20080
20080
  field: "codingAddressReviews",
20081
20081
  kind: "toggle",
20082
- description: "address review feedback"
20082
+ description: "address reviews on Pullfrog-authored PRs"
20083
20083
  },
20084
20084
  {
20085
20085
  key: "fix-ci.own-prs",
@@ -21651,7 +21651,10 @@ function modelHasStoredAuth(params) {
21651
21651
  const authVars = [
21652
21652
  ...getModelEnvVars(slug),
21653
21653
  ...getModelManagedCredentials(slug),
21654
- ...HARNESS_ONLY_CREDENTIALS[getModelProvider(slug)] ?? []
21654
+ ...HARNESS_ONLY_CREDENTIALS[getModelProvider(slug)] ?? [],
21655
+ // the default endpoint's vars above still count: a runner holding no such set runs the
21656
+ // slug as a raw model id on it
21657
+ ...openAICompatibleSetEnvVars(slug)
21655
21658
  ];
21656
21659
  return authVars.some((v) => params.secretNames.includes(v));
21657
21660
  }
@@ -21877,6 +21880,46 @@ function isOssAllowedModel(slug) {
21877
21880
  function getAutoSelectHintModel() {
21878
21881
  return defaultProxyDisplayName;
21879
21882
  }
21883
+ var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
21884
+ function isOpenAICompatibleSetName(name) {
21885
+ return name !== "byok" && name.length <= 32 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
21886
+ }
21887
+ function openAICompatibleSetName(slug) {
21888
+ const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
21889
+ if (!slug.startsWith(prefix)) return void 0;
21890
+ const name = slug.slice(prefix.length);
21891
+ return isOpenAICompatibleSetName(name) ? name : void 0;
21892
+ }
21893
+ function openAICompatibleEnv(name) {
21894
+ const prefix = `OPENAI_COMPATIBLE_${name ? `${name.toUpperCase().replaceAll("-", "_")}_` : ""}`;
21895
+ return {
21896
+ baseUrl: `${prefix}BASE_URL`,
21897
+ apiKey: `${prefix}API_KEY`,
21898
+ model: `${prefix}MODEL`,
21899
+ context: `${prefix}CONTEXT`,
21900
+ maxOutput: `${prefix}MAX_OUTPUT`
21901
+ };
21902
+ }
21903
+ function openAICompatibleSetEnvVars(slug) {
21904
+ const set2 = openAICompatibleSetName(slug);
21905
+ if (!set2) return [];
21906
+ const env = openAICompatibleEnv(set2);
21907
+ return [env.baseUrl, env.apiKey, env.model];
21908
+ }
21909
+ function openAICompatibleSetOf(envName) {
21910
+ const match = /^OPENAI_COMPATIBLE_([A-Z0-9]+(?:_[A-Z0-9]+)*)_(?:BASE_URL|API_KEY|MODEL|CONTEXT|MAX_OUTPUT)$/.exec(
21911
+ envName
21912
+ );
21913
+ const name = match?.[1]?.toLowerCase().replaceAll("_", "-");
21914
+ return name && isOpenAICompatibleSetName(name) ? name : void 0;
21915
+ }
21916
+ function openAICompatibleSets(names) {
21917
+ return [...new Set(names.flatMap((name) => openAICompatibleSetOf(name) ?? []))].sort();
21918
+ }
21919
+ function openAICompatibleSetAlias(set2) {
21920
+ const byok = resolveDisplayAlias(`${OPENAI_COMPATIBLE_PROVIDER}/byok`);
21921
+ return byok && { ...byok, slug: `${OPENAI_COMPATIBLE_PROVIDER}/${set2}`, displayName: set2 };
21922
+ }
21880
21923
  var PROVIDER_GATEWAY_URL_ENV = {
21881
21924
  anthropic: "ANTHROPIC_BASE_URL",
21882
21925
  openai: "OPENAI_BASE_URL"
@@ -23534,6 +23577,7 @@ export {
23534
23577
  isEffortPosition,
23535
23578
  isLeapingIntoActionCommentBody,
23536
23579
  isLiveChecklistCommentBody,
23580
+ isOpenAICompatibleSetName,
23537
23581
  isOssAllowedModel,
23538
23582
  isPullfrog,
23539
23583
  isRouterTier,
@@ -23544,6 +23588,11 @@ export {
23544
23588
  modelIdLabel,
23545
23589
  modes,
23546
23590
  offeredRungs,
23591
+ openAICompatibleEnv,
23592
+ openAICompatibleSetAlias,
23593
+ openAICompatibleSetName,
23594
+ openAICompatibleSetOf,
23595
+ openAICompatibleSets,
23547
23596
  ossFundedModelNames,
23548
23597
  parseCodexAuthBody,
23549
23598
  parseEffortPosition,
package/dist/models.d.ts CHANGED
@@ -464,6 +464,27 @@ export declare const OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT"
464
464
  * `context` + `output` together, so the pair is validated and emitted as a unit.
465
465
  */
466
466
  export declare const OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
467
+ /**
468
+ * a NAMED OpenAI-compatible endpoint: the slug `openai-compatible/<name>` runs on the five
469
+ * `OPENAI_COMPATIBLE_<NAME>_*` values, so one account holds several endpoints and each repo,
470
+ * trigger or `--model=` picks one. `byok` stays the unnamed default on the bare names. 32 is
471
+ * what keeps `OPENAI_COMPATIBLE_<NAME>_MAX_OUTPUT` under the 64-character secret-name cap.
472
+ */
473
+ export declare function isOpenAICompatibleSetName(name: string): boolean;
474
+ /**
475
+ * the set a slug names, by shape alone. `openai-compatible/<x>` is also a raw specifier (model
476
+ * `x` on the default endpoint), so a runner treats it as a set only when it holds that set's values.
477
+ */
478
+ export declare function openAICompatibleSetName(slug: string): string | undefined;
479
+ export declare const OPENAI_COMPATIBLE_FIELDS: readonly ["baseUrl", "apiKey", "model", "context", "maxOutput"];
480
+ /** the env var names one endpoint is configured by: a named set's, or the default's. */
481
+ export declare function openAICompatibleEnv(name?: string): Record<(typeof OPENAI_COMPATIBLE_FIELDS)[number], string>;
482
+ /** the set a variable belongs to: `OPENAI_COMPATIBLE_VLLM_A_API_KEY` → `vllm-a`. */
483
+ export declare function openAICompatibleSetOf(envName: string): string | undefined;
484
+ /** the named endpoints `names` (stored secret or env var names) hold any value of. */
485
+ export declare function openAICompatibleSets(names: string[]): string[];
486
+ /** a named endpoint as a picker entry: the default endpoint's routing alias under its own slug. */
487
+ export declare function openAICompatibleSetAlias(set: string): ModelAlias | undefined;
467
488
  /**
468
489
  * Providers whose endpoint can be re-pointed at a customer gateway, and the env
469
490
  * var that does it. Deliberately a short explicit map rather than a derived
@@ -1,4 +1,13 @@
1
1
  import type { Agent } from "../agents/index.ts";
2
+ /**
3
+ * make the run's named set (`openai-compatible/<name>`) its endpoint: the set's five values
4
+ * replace the bare `OPENAI_COMPATIBLE_*` that validation and the opencode provider read. a value
5
+ * the set lacks is cleared, never inherited, so the default endpoint's key cannot reach the set's
6
+ * URL. returns the set, so setup errors can name its own variables.
7
+ */
8
+ export declare function activateOpenAICompatibleSet(ctx: {
9
+ slug: string | undefined;
10
+ }): string | undefined;
2
11
  /**
3
12
  * resolve the effective model for this run.
4
13
  *
@@ -121,6 +121,8 @@ export declare function validateAgentApiKey(params: {
121
121
  * says nothing about what the user actually configured. */
122
122
  secretsUnavailable?: boolean | undefined;
123
123
  routerUnfunded?: boolean | undefined;
124
+ /** the named OpenAI-compatible set the run is on, from `activateOpenAICompatibleSet`. */
125
+ openAICompatibleSet?: string | undefined;
124
126
  }): void;
125
127
  /**
126
128
  * Detect agent-runtime auth failures that should be reformatted as an actionable
@@ -20,6 +20,11 @@ export interface RunEffort {
20
20
  rung: string | undefined;
21
21
  /** the catalog alias the ladder came from, if we recognized one. */
22
22
  alias: ModelAlias | undefined;
23
+ /**
24
+ * true when the catalog has no ladder for a configured effort and the variants
25
+ * the opencode server declares for the model may stand in (`declaredVariants`).
26
+ */
27
+ declarable: boolean;
23
28
  }
24
29
  /**
25
30
  * the position a subsidised run is pinned to — the `high` rung on the ladder of
@@ -39,4 +44,9 @@ export declare function ossEffortFloor(ctx: {
39
44
  export declare function resolveRunEffort(ctx: {
40
45
  payload: ResolvedPayload;
41
46
  resolvedModel?: string | undefined;
47
+ /**
48
+ * the variant names the opencode server lists for the running model, in its
49
+ * order. only consulted when the catalog has no alias for the model.
50
+ */
51
+ declaredVariants?: readonly string[] | undefined;
42
52
  }): RunEffort;
@@ -14,6 +14,8 @@ export declare function createTempDirectory(): {
14
14
  path: string;
15
15
  [Symbol.dispose]: () => void;
16
16
  };
17
+ /** the `post:` hook's half of `createTempDirectory`: removes the directory a cancelled run left. */
18
+ export declare function removeAbandonedTempDirectory(): void;
17
19
  /**
18
20
  * snapshot-and-delete the GHA runner's known credential leak surfaces inside
19
21
  * `$RUNNER_TEMP` before the agent spawns. without this, a shell-capable agent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {