pullfrog 0.1.104 → 0.1.105

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
@@ -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.105",
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) => {
@@ -196384,13 +196429,16 @@ function buildImplementPlanLink(ctx, issueNumber, commentId) {
196384
196429
  const apiUrl = getApiUrl();
196385
196430
  return `[Implement plan \u2794](${apiUrl}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=implement&comment_id=${commentId})`;
196386
196431
  }
196387
- function buildEnrichmentLinks(ctx) {
196432
+ function runIssueNumber(ctx) {
196433
+ return ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196434
+ }
196435
+ function buildEnrichmentLinks(ctx, issueNumber) {
196388
196436
  const event = ctx.payload.event;
196389
- if (event.trigger !== "issues_opened" || event.is_pr || ctx.payload.prompt || ctx.toolState.selectedMode === "Build") {
196390
- return [];
196391
- }
196437
+ const mode = ctx.toolState.selectedMode;
196438
+ const enriching = mode === "Enrich" || event.trigger === "issues_opened" && !ctx.payload.prompt && mode !== "Build";
196439
+ if (issueNumber === void 0 || event.is_pr || !enriching) return [];
196392
196440
  return [
196393
- `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${event.issue_number}?action=build)`
196441
+ `[Implement a fix \u2794](${getApiUrl()}/trigger/${ctx.repo.owner}/${ctx.repo.name}/${issueNumber}?action=build)`
196394
196442
  ];
196395
196443
  }
196396
196444
  function addFooter(ctx, body) {
@@ -196426,10 +196474,11 @@ function CreateCommentTool(ctx) {
196426
196474
  });
196427
196475
  ctx.toolState.wasUpdated = true;
196428
196476
  log.info(`\xBB created comment ${result.data.id}`);
196429
- if (issueNumber === ctx.payload.event.issue_number) {
196477
+ const ownTarget = issueNumber === runIssueNumber(ctx);
196478
+ if (ownTarget) {
196430
196479
  ctx.toolState.standaloneCommentId = result.data.id;
196431
196480
  }
196432
- const enrichmentLinks = issueNumber === ctx.payload.event.issue_number ? buildEnrichmentLinks(ctx) : [];
196481
+ const enrichmentLinks = ownTarget ? buildEnrichmentLinks(ctx, issueNumber) : [];
196433
196482
  if (commentType === "Plan" || enrichmentLinks.length > 0) {
196434
196483
  if (commentType === "Plan" && result.data.node_id) {
196435
196484
  await patchWorkflowRunFields(ctx, { planCommentNodeId: result.data.node_id });
@@ -196509,9 +196558,9 @@ async function reportProgress(ctx, params) {
196509
196558
  if (params.liveProgress && !ctx.payload.progressComments) {
196510
196559
  return { body, action: "skipped" };
196511
196560
  }
196512
- const issueNumber = ctx.payload.event.issue_number ?? primaryRepoState(ctx.toolState).issueNumber;
196561
+ const issueNumber = runIssueNumber(ctx);
196513
196562
  const isPlanMode = ctx.toolState.selectedMode === "Plan";
196514
- const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx);
196563
+ const enrichmentLinks = params.liveProgress ? [] : buildEnrichmentLinks(ctx, issueNumber);
196515
196564
  const apiCtx = { octokit: ctx.octokit, owner: ctx.repo.owner, repo: ctx.repo.name };
196516
196565
  if (target_plan_comment === true && ctx.toolState.existingPlanCommentId === void 0) {
196517
196566
  log.warning("target_plan_comment requested but no existingPlanCommentId in tool state");
@@ -199522,7 +199571,7 @@ function GetIssueCommentsTool(ctx) {
199522
199571
  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
199572
  parameters: GetIssueComments,
199524
199573
  execute: execute(async ({ issue_number }) => {
199525
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199574
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199526
199575
  const comments = await ctx.octokit.paginate(ctx.octokit.rest.issues.listComments, {
199527
199576
  owner: ctx.repo.owner,
199528
199577
  repo: ctx.repo.name,
@@ -199560,7 +199609,7 @@ function GetIssueEventsTool(ctx) {
199560
199609
  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
199610
  parameters: GetIssueEvents,
199562
199611
  execute: execute(async ({ issue_number }) => {
199563
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199612
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199564
199613
  const events = await ctx.octokit.paginate(ctx.octokit.rest.issues.listEventsForTimeline, {
199565
199614
  owner: ctx.repo.owner,
199566
199615
  repo: ctx.repo.name,
@@ -199644,7 +199693,7 @@ function IssueInfoTool(ctx) {
199644
199693
  tmpdir: ctx.tmpdir,
199645
199694
  githubToken: ctx.githubInstallationToken
199646
199695
  });
199647
- primaryRepoState(ctx.toolState).issueNumber = issue_number;
199696
+ primaryRepoState(ctx.toolState).issueNumber ??= issue_number;
199648
199697
  const hints = [];
199649
199698
  if (data.comments > 0) {
199650
199699
  hints.push("use get_issue_comments to retrieve all comments for this issue");
@@ -201112,8 +201161,11 @@ function SimilarIssuesTool(ctx) {
201112
201161
  mutates: true,
201113
201162
  parameters: SimilarIssues,
201114
201163
  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");
201164
+ const repoState = primaryRepoState(ctx.toolState);
201165
+ repoState.issueNumber ??= input2.issue_number;
201166
+ const subject = ctx.payload.event.issue_number ?? repoState.issueNumber;
201167
+ if (ctx.payload.event.is_pr || subject !== input2.issue_number) {
201168
+ throw new Error(`find_similar_issues is limited to this run's issue, #${subject}`);
201117
201169
  }
201118
201170
  const response = await apiFetch({
201119
201171
  path: `/api/repo/${ctx.repo.owner}/${ctx.repo.name}/issues/${input2.issue_number}/similar`,
@@ -201395,7 +201447,7 @@ function buildCommonTools(ctx, outputSchema) {
201395
201447
  if (hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event })) {
201396
201448
  tools.push(SimilarIssuesTool(ctx));
201397
201449
  }
201398
- if (ctx.payload.event.issue_number !== void 0 && !ctx.payload.event.is_pr) {
201450
+ if (!ctx.payload.event.is_pr) {
201399
201451
  tools.push(SearchIssuesTool(ctx));
201400
201452
  }
201401
201453
  const isStandalone = ctx.payload.event.trigger === "unknown";
@@ -201793,11 +201845,11 @@ function buildOpenAICompatibleSetupError(params) {
201793
201845
  missing: params.missing,
201794
201846
  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
201847
  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"`
201848
+ `${params.env.baseUrl}: https://your-endpoint.example.com/v1`,
201849
+ `${params.env.apiKey}: \${{ secrets.${params.env.apiKey} }}`,
201850
+ `${params.env.model}: <model-id>`,
201851
+ `${params.env.context}: "128000"`,
201852
+ `${params.env.maxOutput}: "16384"`
201801
201853
  ],
201802
201854
  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
201855
  guide: "https://docs.pullfrog.com/openai-compatible"
@@ -201831,17 +201883,16 @@ function hasPositiveNumberEnvVar(name) {
201831
201883
  return Number(process.env[name]) > 0;
201832
201884
  }
201833
201885
  function validateOpenAICompatibleSetup(params) {
201886
+ const env2 = openAICompatibleEnv(params.set);
201834
201887
  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);
201888
+ if (!hasEnvVar2(env2.baseUrl)) missing.push(env2.baseUrl);
201889
+ if (!hasEnvVar2(env2.apiKey)) missing.push(env2.apiKey);
201890
+ if (!hasEnvVar2(env2.model)) missing.push(env2.model);
201891
+ if (!hasPositiveNumberEnvVar(env2.context)) missing.push(env2.context);
201892
+ if (!hasPositiveNumberEnvVar(env2.maxOutput)) missing.push(env2.maxOutput);
201842
201893
  if (missing.length > 0) {
201843
201894
  throw new Error(
201844
- buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
201895
+ buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing, env: env2 })
201845
201896
  );
201846
201897
  }
201847
201898
  }
@@ -201897,7 +201948,11 @@ function validateAgentApiKey(params) {
201897
201948
  return;
201898
201949
  }
201899
201950
  if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
201900
- validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
201951
+ validateOpenAICompatibleSetup({
201952
+ owner: params.owner,
201953
+ name: params.name,
201954
+ set: params.openAICompatibleSet
201955
+ });
201901
201956
  return;
201902
201957
  }
201903
201958
  if (params.model.startsWith(`${AZURE_PROVIDER}/`)) {
@@ -202211,17 +202266,37 @@ function resolveSlug(slug2) {
202211
202266
  }
202212
202267
  return `${AZURE_PROVIDER}/${deployment}`;
202213
202268
  }
202214
- if (alias?.routing === "openai-compatible") {
202215
- const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
202269
+ const set2 = heldOpenAICompatibleSet(slug2);
202270
+ if (alias?.routing === "openai-compatible" || set2) {
202271
+ const modelEnv = openAICompatibleEnv(set2).model;
202272
+ const modelId = process.env[modelEnv]?.trim();
202216
202273
  if (!modelId) {
202217
202274
  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)`
202275
+ `\`${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
202276
  );
202220
202277
  }
202221
202278
  return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
202222
202279
  }
202223
202280
  return resolveCliModel(slug2);
202224
202281
  }
202282
+ function heldOpenAICompatibleSet(slug2) {
202283
+ const set2 = openAICompatibleSetName(slug2);
202284
+ return set2 && Object.values(openAICompatibleEnv(set2)).some(hasEnvVar3) ? set2 : void 0;
202285
+ }
202286
+ function activateOpenAICompatibleSet(ctx) {
202287
+ const slug2 = process.env.PULLFROG_MODEL?.trim() || ctx.slug?.trim();
202288
+ const set2 = slug2 ? heldOpenAICompatibleSet(slug2) : void 0;
202289
+ if (!set2) return void 0;
202290
+ const from = openAICompatibleEnv(set2);
202291
+ const to = openAICompatibleEnv();
202292
+ for (const field of OPENAI_COMPATIBLE_FIELDS) {
202293
+ const value2 = process.env[from[field]];
202294
+ if (value2 === void 0) delete process.env[to[field]];
202295
+ else process.env[to[field]] = value2;
202296
+ }
202297
+ log.info(`\xBB using the OpenAI-compatible endpoint "${set2}"`);
202298
+ return set2;
202299
+ }
202225
202300
  function resolveModel(ctx) {
202226
202301
  const envModel = process.env.PULLFROG_MODEL?.trim();
202227
202302
  if (envModel) {
@@ -203264,7 +203339,7 @@ Rules:
203264
203339
  ### GitHub
203265
203340
 
203266
203341
  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 }) ? `
203342
+ ${ctx.payload.event.issue_number !== void 0 && hasSimilarIssues({ repoIntelligence: ctx.repoIntelligence, event: ctx.payload.event }) ? `
203268
203343
  #### Duplicate detection (enabled for this repository)
203269
203344
 
203270
203345
  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 +205218,9 @@ function resolveModelForLog(ctx) {
205143
205218
  function resolveEffortForLog(ctx) {
205144
205219
  const effort = resolveRunEffort(ctx);
205145
205220
  if (!ctx.resolvedModel && !ctx.payload.proxyModel) return "pending \u2014 model not chosen yet";
205221
+ if (effort.declarable && ctx.agentName === "opencode") {
205222
+ return "pending \u2014 reading the variants opencode declares";
205223
+ }
205146
205224
  if (!effort.alias) return "not applied \u2014 model not recognized";
205147
205225
  if (!effort.rung) return "n/a (model has no effort control)";
205148
205226
  return effort.configured ? effort.rung : `${effort.rung} (default)`;
@@ -205161,9 +205239,7 @@ function logRunStartup(ctx) {
205161
205239
  log.info(
205162
205240
  `\xBB model: ${resolveModelForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205163
205241
  );
205164
- log.info(
205165
- `\xBB effort: ${resolveEffortForLog({ payload: ctx.payload, resolvedModel: ctx.resolvedModel })}`
205166
- );
205242
+ log.info(`\xBB effort: ${resolveEffortForLog(ctx)}`);
205167
205243
  const routing = ctx.payload.routing;
205168
205244
  if (routing) {
205169
205245
  log.info(
@@ -205434,6 +205510,7 @@ async function main() {
205434
205510
  const count = Object.keys(runContext.dbSecrets).length;
205435
205511
  if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
205436
205512
  }
205513
+ const openAICompatibleSet = activateOpenAICompatibleSet({ slug: payload.model });
205437
205514
  await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
205438
205515
  installCodexAuth();
205439
205516
  installXaiAuth();
@@ -205582,7 +205659,8 @@ async function main() {
205582
205659
  owner: runContext.repo.owner,
205583
205660
  name: runContext.repo.name,
205584
205661
  secretsUnavailable: runContext.secretsUnavailable,
205585
- routerUnfunded: runContext.routerUnfunded
205662
+ routerUnfunded: runContext.routerUnfunded,
205663
+ openAICompatibleSet
205586
205664
  });
205587
205665
  } catch (missingKey) {
205588
205666
  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.105",
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {