pullfrog 0.1.45 → 0.1.46

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.
@@ -16,6 +16,30 @@ export type OpenCodeConfig = {
16
16
  export declare function geminiHighThinkingOverrides(): Record<string, {
17
17
  options: object;
18
18
  }>;
19
+ interface OpenAICompatibleProviderEntry {
20
+ npm: string;
21
+ name: string;
22
+ options: {
23
+ baseURL: string | undefined;
24
+ apiKey: string | undefined;
25
+ };
26
+ models: Record<string, {
27
+ name: string;
28
+ limit: ModelLimit;
29
+ }>;
30
+ }
31
+ interface ModelLimit {
32
+ context: number;
33
+ output: number;
34
+ }
35
+ /**
36
+ * OpenAI-compatible provider block for OPENCODE_CONFIG_CONTENT. opencode has no
37
+ * native provider for an arbitrary gateway, so inject one via
38
+ * `@ai-sdk/openai-compatible` when the run targets `openai-compatible/<model>`;
39
+ * `{}` otherwise so the caller can spread it unconditionally. base URL + key are
40
+ * guaranteed present by `validateOpenAICompatibleSetup`.
41
+ */
42
+ export declare function openAICompatibleProvider(model: string | undefined): Record<string, OpenAICompatibleProviderEntry>;
19
43
  /**
20
44
  * Build the `provider.openrouter.models[id].options` map that pins every Kimi
21
45
  * K2 OpenRouter alias away from the Enforcer-less providers via OpenRouter's
@@ -75,3 +99,4 @@ export declare function installOpencodeCli(params: {
75
99
  binPath: string;
76
100
  }): Promise<string>;
77
101
  export declare function autoSelectModel(): string | undefined;
102
+ export {};
package/dist/cli.mjs CHANGED
@@ -102119,6 +102119,28 @@ var providers = {
102119
102119
  }
102120
102120
  }
102121
102121
  }),
102122
+ "openai-compatible": provider({
102123
+ // "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
102124
+ // menu reads `Custom › OpenAI-compatible` and a second custom backend (a
102125
+ // different wire format, say) slots in beside it without a rename. the
102126
+ // provider KEY stays `openai-compatible` — it's the stored slug and the
102127
+ // `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
102128
+ displayName: "Custom",
102129
+ // bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
102130
+ // Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
102131
+ // key + model ID are all supplied via env; nothing is cataloged or bumped.
102132
+ envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
102133
+ models: {
102134
+ // single routing entry — the actual model ID is read from
102135
+ // OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
102136
+ // via `@ai-sdk/openai-compatible`.
102137
+ byok: {
102138
+ displayName: "OpenAI-compatible",
102139
+ resolve: "openai-compatible",
102140
+ routing: "openai-compatible"
102141
+ }
102142
+ }
102143
+ }),
102122
102144
  openrouter: provider({
102123
102145
  displayName: "OpenRouter",
102124
102146
  envVars: ["OPENROUTER_API_KEY"],
@@ -102336,6 +102358,12 @@ var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
102336
102358
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
102337
102359
  var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
102338
102360
  var VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
102361
+ var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
102362
+ var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
102363
+ var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
102364
+ var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
102365
+ var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
102366
+ var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
102339
102367
  function isBedrockAnthropicId(bedrockModelId) {
102340
102368
  return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
102341
102369
  }
@@ -103022,7 +103050,7 @@ var import_semver = __toESM(require_semver2(), 1);
103022
103050
  // package.json
103023
103051
  var package_default = {
103024
103052
  name: "pullfrog",
103025
- version: "0.1.45",
103053
+ version: "0.1.46",
103026
103054
  type: "module",
103027
103055
  bin: {
103028
103056
  pullfrog: "dist/cli.mjs",
@@ -110769,8 +110797,9 @@ function readModels(cliPath) {
110769
110797
  stdio: ["ignore", "pipe", "pipe"]
110770
110798
  });
110771
110799
  if (result.status !== 0) {
110772
- failure = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim() || void 0;
110773
- log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure ?? result.error}`);
110800
+ const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
110801
+ failure = stderr || result.error?.message;
110802
+ log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
110774
110803
  return /* @__PURE__ */ new Set();
110775
110804
  }
110776
110805
  failure = void 0;
@@ -110828,6 +110857,29 @@ function geminiHighThinkingOverrides() {
110828
110857
  ])
110829
110858
  );
110830
110859
  }
110860
+ function openAICompatibleLimit() {
110861
+ return {
110862
+ context: Number(process.env[OPENAI_COMPATIBLE_CONTEXT_ENV]),
110863
+ output: Number(process.env[OPENAI_COMPATIBLE_MAX_OUTPUT_ENV])
110864
+ };
110865
+ }
110866
+ function openAICompatibleProvider(model) {
110867
+ const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
110868
+ if (!model?.startsWith(prefix)) return {};
110869
+ const modelId = model.slice(prefix.length);
110870
+ return {
110871
+ [OPENAI_COMPATIBLE_PROVIDER]: {
110872
+ npm: "@ai-sdk/openai-compatible",
110873
+ name: "OpenAI-compatible",
110874
+ options: {
110875
+ // trailing slash would double up against opencode's `/chat/completions` join
110876
+ baseURL: process.env[OPENAI_COMPATIBLE_BASE_URL_ENV]?.replace(/\/$/, ""),
110877
+ apiKey: process.env[OPENAI_COMPATIBLE_API_KEY_ENV]
110878
+ },
110879
+ models: { [modelId]: { name: modelId, limit: openAICompatibleLimit() } }
110880
+ }
110881
+ };
110882
+ }
110831
110883
  var KIMI_ENFORCERLESS_PROVIDERS = ["siliconflow", "together"];
110832
110884
  function kimiOpenRouterProviderOverrides() {
110833
110885
  return Object.fromEntries(
@@ -110919,7 +110971,8 @@ function buildSecurityConfig(ctx, model) {
110919
110971
  google: { models: geminiHighThinkingOverrides() },
110920
110972
  openrouter: {
110921
110973
  models: { ...deepseekHighEffortOverrides(), ...kimiOpenRouterProviderOverrides() }
110922
- }
110974
+ },
110975
+ ...openAICompatibleProvider(model)
110923
110976
  }
110924
110977
  };
110925
110978
  if (model) {
@@ -165185,6 +165238,15 @@ function resolveSlug(slug2) {
165185
165238
  }
165186
165239
  return vertexId;
165187
165240
  }
165241
+ if (alias?.routing === "openai-compatible") {
165242
+ const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
165243
+ if (!modelId) {
165244
+ throw new Error(
165245
+ `${OPENAI_COMPATIBLE_MODEL_ENV} env var is required when the model is set to "${slug2}". set it to the model ID served by your OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway or DashScope model). see https://docs.pullfrog.com/openai-compatible for setup.`
165246
+ );
165247
+ }
165248
+ return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
165249
+ }
165188
165250
  return resolveCliModel(slug2);
165189
165251
  }
165190
165252
  function resolveModel(ctx) {
@@ -165234,6 +165296,21 @@ function resolveAgent(ctx) {
165234
165296
 
165235
165297
  // utils/apiKeys.ts
165236
165298
  var MISSING_KEY_MARKER = "no API key found";
165299
+ var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
165300
+ function buildKeyError(params) {
165301
+ return params.secretsUnavailable ? buildSecretsUnavailableError(params) : buildMissingApiKeyError(params);
165302
+ }
165303
+ function buildSecretsUnavailableError(params) {
165304
+ const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
165305
+ const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
165306
+ return [
165307
+ `**Pullfrog ${SECRETS_UNAVAILABLE_MARKER}${modelClause} on this run.** The key is still stored \u2014 the runner just couldn't fetch it, so this is a transient failure on our side, not a missing key.`,
165308
+ "",
165309
+ "**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
165310
+ "",
165311
+ `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
165312
+ ].join("\n");
165313
+ }
165237
165314
  function buildMissingApiKeyError(params) {
165238
165315
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
165239
165316
  const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
@@ -165276,10 +165353,47 @@ add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then
165276
165353
 
165277
165354
  for full setup instructions, see https://docs.pullfrog.com/vertex`;
165278
165355
  }
165356
+ function buildOpenAICompatibleSetupError(params) {
165357
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
165358
+ return `OpenAI-compatible model selected but required configuration is missing: ${params.missing.join(", ")}.
165359
+
165360
+ only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
165361
+
165362
+ ${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
165363
+ ${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
165364
+ ${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
165365
+ ${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
165366
+ ${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
165367
+
165368
+ set the last two to the real limits of the model your endpoint serves. Pullfrog can't
165369
+ discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
165370
+ capped at 32000 tokens (rejected outright by models with a smaller cap) and
165371
+ auto-compaction is disabled, so long runs grow until your endpoint refuses them.
165372
+
165373
+ for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
165374
+ }
165279
165375
  function hasEnvVar3(name) {
165280
165376
  const value2 = process.env[name];
165281
165377
  return typeof value2 === "string" && value2.length > 0;
165282
165378
  }
165379
+ function hasPositiveNumberEnvVar(name) {
165380
+ return Number(process.env[name]) > 0;
165381
+ }
165382
+ function validateOpenAICompatibleSetup(params) {
165383
+ const missing = [];
165384
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
165385
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
165386
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
165387
+ if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
165388
+ missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
165389
+ if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
165390
+ missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
165391
+ if (missing.length > 0) {
165392
+ throw new Error(
165393
+ buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
165394
+ );
165395
+ }
165396
+ }
165283
165397
  function validateBedrockSetup(params) {
165284
165398
  const hasAuth = hasEnvVar3("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar3("AWS_ACCESS_KEY_ID") && hasEnvVar3("AWS_SECRET_ACCESS_KEY");
165285
165399
  const missing = [];
@@ -165314,6 +165428,10 @@ function validateAgentApiKey(params) {
165314
165428
  validateVertexSetup({ owner: params.owner, name: params.name });
165315
165429
  return;
165316
165430
  }
165431
+ if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
165432
+ validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
165433
+ return;
165434
+ }
165317
165435
  if (!params.model.includes("/")) {
165318
165436
  if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
165319
165437
  validateVertexSetup({ owner: params.owner, name: params.name });
@@ -165326,23 +165444,46 @@ function validateAgentApiKey(params) {
165326
165444
  if (params.authorized.has(params.model)) return;
165327
165445
  const reason = getModelsFailure();
165328
165446
  if (reason) throw new Error(reason);
165447
+ if (getModelEnvVars(params.model).some(hasEnvVar3)) return;
165329
165448
  throw new Error(
165330
- buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
165449
+ buildKeyError({
165450
+ owner: params.owner,
165451
+ name: params.name,
165452
+ model: params.model,
165453
+ secretsUnavailable: params.secretsUnavailable
165454
+ })
165331
165455
  );
165332
165456
  }
165333
165457
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
165334
165458
  throw new Error(
165335
- buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
165459
+ buildKeyError({
165460
+ owner: params.owner,
165461
+ name: params.name,
165462
+ model: params.model,
165463
+ secretsUnavailable: params.secretsUnavailable
165464
+ })
165336
165465
  );
165337
165466
  }
165338
165467
  if (params.agent.name === "opencode") {
165339
165468
  if (params.authorized.size > 0) return;
165340
165469
  const reason = getModelsFailure();
165341
165470
  if (reason) throw new Error(reason);
165342
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
165471
+ throw new Error(
165472
+ buildKeyError({
165473
+ owner: params.owner,
165474
+ name: params.name,
165475
+ secretsUnavailable: params.secretsUnavailable
165476
+ })
165477
+ );
165343
165478
  }
165344
165479
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
165345
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
165480
+ throw new Error(
165481
+ buildKeyError({
165482
+ owner: params.owner,
165483
+ name: params.name,
165484
+ secretsUnavailable: params.secretsUnavailable
165485
+ })
165486
+ );
165346
165487
  }
165347
165488
  function isApiKeyAuthError(text) {
165348
165489
  if (!text) return false;
@@ -166027,7 +166168,7 @@ async function persistXrepoLearnings(ctx) {
166027
166168
  // utils/modelAccess.ts
166028
166169
  function decideModelAccess(input) {
166029
166170
  if (!input.modelExplicit || !input.model) return { kind: "ok" };
166030
- const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/"));
166171
+ const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`));
166031
166172
  if (input.proxyActive) {
166032
166173
  const target = resolveOpenRouterModel(input.model);
166033
166174
  if (input.oss) {
@@ -166836,6 +166977,10 @@ var defaultRunContext = {
166836
166977
  oss: false,
166837
166978
  plan: "none"
166838
166979
  };
166980
+ var unknownSecretsRunContext = {
166981
+ ...defaultRunContext,
166982
+ secretsUnavailable: true
166983
+ };
166839
166984
  async function fetchRunContext(params) {
166840
166985
  const timeoutMs = 3e4;
166841
166986
  const controller = new AbortController();
@@ -166854,7 +166999,7 @@ async function fetchRunContext(params) {
166854
166999
  });
166855
167000
  clearTimeout(timeoutId);
166856
167001
  if (!response.ok) {
166857
- return defaultRunContext;
167002
+ return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
166858
167003
  }
166859
167004
  const data = await response.json();
166860
167005
  if (data === null) {
@@ -166878,11 +167023,12 @@ async function fetchRunContext(params) {
166878
167023
  oss: data.oss ?? false,
166879
167024
  plan: data.plan ?? "none",
166880
167025
  proxyModel: data.proxyModel,
166881
- dbSecrets: data.dbSecrets
167026
+ dbSecrets: data.dbSecrets,
167027
+ secretsUnavailable: data.secretsUnavailable
166882
167028
  };
166883
167029
  } catch {
166884
167030
  clearTimeout(timeoutId);
166885
- return defaultRunContext;
167031
+ return unknownSecretsRunContext;
166886
167032
  }
166887
167033
  }
166888
167034
 
@@ -166904,7 +167050,10 @@ async function resolveRunContextData(params) {
166904
167050
  const repoContext = parseRepoContext();
166905
167051
  let oidcToken;
166906
167052
  try {
166907
- oidcToken = await core9.getIDToken("pullfrog-api");
167053
+ oidcToken = await op(() => core9.getIDToken("pullfrog-api"), {
167054
+ name: "OIDC mint",
167055
+ retries: process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? [200, 1e3] : []
167056
+ })();
166908
167057
  } catch {
166909
167058
  }
166910
167059
  const [repoResponse, runContext] = await Promise.all([
@@ -166926,7 +167075,10 @@ async function resolveRunContextData(params) {
166926
167075
  oss: runContext.oss,
166927
167076
  plan: runContext.plan,
166928
167077
  proxyModel: runContext.proxyModel,
166929
- dbSecrets: runContext.dbSecrets
167078
+ dbSecrets: runContext.dbSecrets,
167079
+ // a failed mint on a runner that should have been able to mint is the same
167080
+ // outcome as the server-side failure: the run never sees stored secrets.
167081
+ secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0
166930
167082
  };
166931
167083
  }
166932
167084
 
@@ -167091,6 +167243,9 @@ function renderRunError(input) {
167091
167243
  if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
167092
167244
  return { summary: input.errorMessage, comment: input.errorMessage };
167093
167245
  }
167246
+ if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
167247
+ return { summary: input.errorMessage, comment: input.errorMessage };
167248
+ }
167094
167249
  const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
167095
167250
  const hangBody = isHang ? formatAgentHangBody({
167096
167251
  diagnostic: input.agentDiagnostic,
@@ -167825,7 +167980,8 @@ async function main() {
167825
167980
  model: effectiveModel,
167826
167981
  authorized: getAuthorizedModels(),
167827
167982
  owner: runContext.repo.owner,
167828
- name: runContext.repo.name
167983
+ name: runContext.repo.name,
167984
+ secretsUnavailable: runContext.secretsUnavailable
167829
167985
  });
167830
167986
  }
167831
167987
  await setupGit({
@@ -168322,7 +168478,7 @@ function link(text, url4) {
168322
168478
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
168323
168479
  }
168324
168480
  function buildProviders() {
168325
- return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter" && key !== "bedrock").map(([key, config3]) => {
168481
+ return Object.entries(providers).filter(([key]) => key !== "opencode" && key !== "openrouter").map(([key, config3]) => {
168326
168482
  const aliases = modelAliases.filter(
168327
168483
  (a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
168328
168484
  );
@@ -168342,7 +168498,7 @@ function buildProviders() {
168342
168498
  hint: a === recommended ? "recommended" : void 0
168343
168499
  }))
168344
168500
  };
168345
- });
168501
+ }).filter((p2) => p2.models.length > 0);
168346
168502
  }
168347
168503
  var CLI_PROVIDERS = buildProviders();
168348
168504
  function resolveModelProvider(slug2) {
@@ -169152,7 +169308,7 @@ async function runCli4(input) {
169152
169308
  }
169153
169309
 
169154
169310
  // cli.ts
169155
- var VERSION10 = "0.1.45";
169311
+ var VERSION10 = "0.1.46";
169156
169312
  var bin = basename2(process.argv[1] || "");
169157
169313
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
169158
169314
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -100190,6 +100190,28 @@ var providers = {
100190
100190
  }
100191
100191
  }
100192
100192
  }),
100193
+ "openai-compatible": provider({
100194
+ // "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
100195
+ // menu reads `Custom › OpenAI-compatible` and a second custom backend (a
100196
+ // different wire format, say) slots in beside it without a rename. the
100197
+ // provider KEY stays `openai-compatible` — it's the stored slug and the
100198
+ // `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
100199
+ displayName: "Custom",
100200
+ // bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
100201
+ // Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
100202
+ // key + model ID are all supplied via env; nothing is cataloged or bumped.
100203
+ envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
100204
+ models: {
100205
+ // single routing entry — the actual model ID is read from
100206
+ // OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
100207
+ // via `@ai-sdk/openai-compatible`.
100208
+ byok: {
100209
+ displayName: "OpenAI-compatible",
100210
+ resolve: "openai-compatible",
100211
+ routing: "openai-compatible"
100212
+ }
100213
+ }
100214
+ }),
100193
100215
  openrouter: provider({
100194
100216
  displayName: "OpenRouter",
100195
100217
  envVars: ["OPENROUTER_API_KEY"],
@@ -100407,6 +100429,12 @@ var DEFAULT_PROXY_MODEL = defaultProxyAlias.openRouterResolve;
100407
100429
  var defaultProxyDisplayName = defaultProxyAlias.displayName;
100408
100430
  var BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
100409
100431
  var VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
100432
+ var OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
100433
+ var OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
100434
+ var OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
100435
+ var OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
100436
+ var OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
100437
+ var OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
100410
100438
  function isBedrockAnthropicId(bedrockModelId) {
100411
100439
  return bedrockModelId.toLowerCase().split(/[./:]/).includes("anthropic");
100412
100440
  }
@@ -101093,7 +101121,7 @@ var import_semver = __toESM(require_semver2(), 1);
101093
101121
  // package.json
101094
101122
  var package_default = {
101095
101123
  name: "pullfrog",
101096
- version: "0.1.45",
101124
+ version: "0.1.46",
101097
101125
  type: "module",
101098
101126
  bin: {
101099
101127
  pullfrog: "dist/cli.mjs",
@@ -108882,8 +108910,9 @@ function readModels(cliPath) {
108882
108910
  stdio: ["ignore", "pipe", "pipe"]
108883
108911
  });
108884
108912
  if (result.status !== 0) {
108885
- failure = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim() || void 0;
108886
- log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure ?? result.error}`);
108913
+ const stderr = (result.stderr ?? "").replace(ANSI_PATTERN, "").trim();
108914
+ failure = stderr || result.error?.message;
108915
+ log.debug(`\xBB \`opencode models\` failed (${result.status}): ${failure}`);
108887
108916
  return /* @__PURE__ */ new Set();
108888
108917
  }
108889
108918
  failure = void 0;
@@ -108941,6 +108970,29 @@ function geminiHighThinkingOverrides() {
108941
108970
  ])
108942
108971
  );
108943
108972
  }
108973
+ function openAICompatibleLimit() {
108974
+ return {
108975
+ context: Number(process.env[OPENAI_COMPATIBLE_CONTEXT_ENV]),
108976
+ output: Number(process.env[OPENAI_COMPATIBLE_MAX_OUTPUT_ENV])
108977
+ };
108978
+ }
108979
+ function openAICompatibleProvider(model) {
108980
+ const prefix = `${OPENAI_COMPATIBLE_PROVIDER}/`;
108981
+ if (!model?.startsWith(prefix)) return {};
108982
+ const modelId = model.slice(prefix.length);
108983
+ return {
108984
+ [OPENAI_COMPATIBLE_PROVIDER]: {
108985
+ npm: "@ai-sdk/openai-compatible",
108986
+ name: "OpenAI-compatible",
108987
+ options: {
108988
+ // trailing slash would double up against opencode's `/chat/completions` join
108989
+ baseURL: process.env[OPENAI_COMPATIBLE_BASE_URL_ENV]?.replace(/\/$/, ""),
108990
+ apiKey: process.env[OPENAI_COMPATIBLE_API_KEY_ENV]
108991
+ },
108992
+ models: { [modelId]: { name: modelId, limit: openAICompatibleLimit() } }
108993
+ }
108994
+ };
108995
+ }
108944
108996
  var KIMI_ENFORCERLESS_PROVIDERS = ["siliconflow", "together"];
108945
108997
  function kimiOpenRouterProviderOverrides() {
108946
108998
  return Object.fromEntries(
@@ -109032,7 +109084,8 @@ function buildSecurityConfig(ctx, model) {
109032
109084
  google: { models: geminiHighThinkingOverrides() },
109033
109085
  openrouter: {
109034
109086
  models: { ...deepseekHighEffortOverrides(), ...kimiOpenRouterProviderOverrides() }
109035
- }
109087
+ },
109088
+ ...openAICompatibleProvider(model)
109036
109089
  }
109037
109090
  };
109038
109091
  if (model) {
@@ -163298,6 +163351,15 @@ function resolveSlug(slug2) {
163298
163351
  }
163299
163352
  return vertexId;
163300
163353
  }
163354
+ if (alias?.routing === "openai-compatible") {
163355
+ const modelId = process.env[OPENAI_COMPATIBLE_MODEL_ENV]?.trim();
163356
+ if (!modelId) {
163357
+ throw new Error(
163358
+ `${OPENAI_COMPATIBLE_MODEL_ENV} env var is required when the model is set to "${slug2}". set it to the model ID served by your OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway or DashScope model). see https://docs.pullfrog.com/openai-compatible for setup.`
163359
+ );
163360
+ }
163361
+ return `${OPENAI_COMPATIBLE_PROVIDER}/${modelId}`;
163362
+ }
163301
163363
  return resolveCliModel(slug2);
163302
163364
  }
163303
163365
  function resolveModel(ctx) {
@@ -163347,6 +163409,21 @@ function resolveAgent(ctx) {
163347
163409
 
163348
163410
  // utils/apiKeys.ts
163349
163411
  var MISSING_KEY_MARKER = "no API key found";
163412
+ var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
163413
+ function buildKeyError(params) {
163414
+ return params.secretsUnavailable ? buildSecretsUnavailableError(params) : buildMissingApiKeyError(params);
163415
+ }
163416
+ function buildSecretsUnavailableError(params) {
163417
+ const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
163418
+ const modelClause = params.model ? ` needed for \`${params.model}\`` : "";
163419
+ return [
163420
+ `**Pullfrog ${SECRETS_UNAVAILABLE_MARKER}${modelClause} on this run.** The key is still stored \u2014 the runner just couldn't fetch it, so this is a transient failure on our side, not a missing key.`,
163421
+ "",
163422
+ "**To fix:** re-run the job. If it keeps happening, tell us in Discord.",
163423
+ "",
163424
+ `[Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
163425
+ ].join("\n");
163426
+ }
163350
163427
  function buildMissingApiKeyError(params) {
163351
163428
  const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
163352
163429
  const settingsUrl = `${getApiUrl()}/console/${params.owner}/${params.name}`;
@@ -163389,10 +163466,47 @@ add the missing secret(s) to your GitHub repository at ${githubSecretsUrl}, then
163389
163466
 
163390
163467
  for full setup instructions, see https://docs.pullfrog.com/vertex`;
163391
163468
  }
163469
+ function buildOpenAICompatibleSetupError(params) {
163470
+ const githubSecretsUrl = `https://github.com/${params.owner}/${params.name}/settings/secrets/actions`;
163471
+ return `OpenAI-compatible model selected but required configuration is missing: ${params.missing.join(", ")}.
163472
+
163473
+ only the API key is sensitive \u2014 add it as a secret at ${githubSecretsUrl}. everything else is plain workflow \`env:\`:
163474
+
163475
+ ${OPENAI_COMPATIBLE_BASE_URL_ENV}: https://your-endpoint.example.com/v1
163476
+ ${OPENAI_COMPATIBLE_API_KEY_ENV}: \${{ secrets.${OPENAI_COMPATIBLE_API_KEY_ENV} }}
163477
+ ${OPENAI_COMPATIBLE_MODEL_ENV}: <model-id>
163478
+ ${OPENAI_COMPATIBLE_CONTEXT_ENV}: "128000"
163479
+ ${OPENAI_COMPATIBLE_MAX_OUTPUT_ENV}: "16384"
163480
+
163481
+ set the last two to the real limits of the model your endpoint serves. Pullfrog can't
163482
+ discover them \u2014 your endpoint owns the model catalog \u2014 and without them completions are
163483
+ capped at 32000 tokens (rejected outright by models with a smaller cap) and
163484
+ auto-compaction is disabled, so long runs grow until your endpoint refuses them.
163485
+
163486
+ for full setup instructions, see https://docs.pullfrog.com/openai-compatible`;
163487
+ }
163392
163488
  function hasEnvVar3(name) {
163393
163489
  const value2 = process.env[name];
163394
163490
  return typeof value2 === "string" && value2.length > 0;
163395
163491
  }
163492
+ function hasPositiveNumberEnvVar(name) {
163493
+ return Number(process.env[name]) > 0;
163494
+ }
163495
+ function validateOpenAICompatibleSetup(params) {
163496
+ const missing = [];
163497
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_BASE_URL_ENV)) missing.push(OPENAI_COMPATIBLE_BASE_URL_ENV);
163498
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_API_KEY_ENV)) missing.push(OPENAI_COMPATIBLE_API_KEY_ENV);
163499
+ if (!hasEnvVar3(OPENAI_COMPATIBLE_MODEL_ENV)) missing.push(OPENAI_COMPATIBLE_MODEL_ENV);
163500
+ if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_CONTEXT_ENV))
163501
+ missing.push(OPENAI_COMPATIBLE_CONTEXT_ENV);
163502
+ if (!hasPositiveNumberEnvVar(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV))
163503
+ missing.push(OPENAI_COMPATIBLE_MAX_OUTPUT_ENV);
163504
+ if (missing.length > 0) {
163505
+ throw new Error(
163506
+ buildOpenAICompatibleSetupError({ owner: params.owner, name: params.name, missing })
163507
+ );
163508
+ }
163509
+ }
163396
163510
  function validateBedrockSetup(params) {
163397
163511
  const hasAuth = hasEnvVar3("AWS_BEARER_TOKEN_BEDROCK") || hasEnvVar3("AWS_ACCESS_KEY_ID") && hasEnvVar3("AWS_SECRET_ACCESS_KEY");
163398
163512
  const missing = [];
@@ -163427,6 +163541,10 @@ function validateAgentApiKey(params) {
163427
163541
  validateVertexSetup({ owner: params.owner, name: params.name });
163428
163542
  return;
163429
163543
  }
163544
+ if (params.model.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`)) {
163545
+ validateOpenAICompatibleSetup({ owner: params.owner, name: params.name });
163546
+ return;
163547
+ }
163430
163548
  if (!params.model.includes("/")) {
163431
163549
  if (process.env[VERTEX_MODEL_ID_ENV]?.trim() === params.model) {
163432
163550
  validateVertexSetup({ owner: params.owner, name: params.name });
@@ -163439,23 +163557,46 @@ function validateAgentApiKey(params) {
163439
163557
  if (params.authorized.has(params.model)) return;
163440
163558
  const reason = getModelsFailure();
163441
163559
  if (reason) throw new Error(reason);
163560
+ if (getModelEnvVars(params.model).some(hasEnvVar3)) return;
163442
163561
  throw new Error(
163443
- buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
163562
+ buildKeyError({
163563
+ owner: params.owner,
163564
+ name: params.name,
163565
+ model: params.model,
163566
+ secretsUnavailable: params.secretsUnavailable
163567
+ })
163444
163568
  );
163445
163569
  }
163446
163570
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
163447
163571
  throw new Error(
163448
- buildMissingApiKeyError({ owner: params.owner, name: params.name, model: params.model })
163572
+ buildKeyError({
163573
+ owner: params.owner,
163574
+ name: params.name,
163575
+ model: params.model,
163576
+ secretsUnavailable: params.secretsUnavailable
163577
+ })
163449
163578
  );
163450
163579
  }
163451
163580
  if (params.agent.name === "opencode") {
163452
163581
  if (params.authorized.size > 0) return;
163453
163582
  const reason = getModelsFailure();
163454
163583
  if (reason) throw new Error(reason);
163455
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
163584
+ throw new Error(
163585
+ buildKeyError({
163586
+ owner: params.owner,
163587
+ name: params.name,
163588
+ secretsUnavailable: params.secretsUnavailable
163589
+ })
163590
+ );
163456
163591
  }
163457
163592
  if (hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN")) return;
163458
- throw new Error(buildMissingApiKeyError({ owner: params.owner, name: params.name }));
163593
+ throw new Error(
163594
+ buildKeyError({
163595
+ owner: params.owner,
163596
+ name: params.name,
163597
+ secretsUnavailable: params.secretsUnavailable
163598
+ })
163599
+ );
163459
163600
  }
163460
163601
  function isApiKeyAuthError(text) {
163461
163602
  if (!text) return false;
@@ -164140,7 +164281,7 @@ async function persistXrepoLearnings(ctx) {
164140
164281
  // utils/modelAccess.ts
164141
164282
  function decideModelAccess(input) {
164142
164283
  if (!input.modelExplicit || !input.model) return { kind: "ok" };
164143
- const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/"));
164284
+ const byokAuthorized = !!input.resolvedModel && (input.authorized.has(input.resolvedModel) || !input.resolvedModel.includes("/") || input.resolvedModel.startsWith(`${OPENAI_COMPATIBLE_PROVIDER}/`));
164144
164285
  if (input.proxyActive) {
164145
164286
  const target = resolveOpenRouterModel(input.model);
164146
164287
  if (input.oss) {
@@ -164949,6 +165090,10 @@ var defaultRunContext = {
164949
165090
  oss: false,
164950
165091
  plan: "none"
164951
165092
  };
165093
+ var unknownSecretsRunContext = {
165094
+ ...defaultRunContext,
165095
+ secretsUnavailable: true
165096
+ };
164952
165097
  async function fetchRunContext(params) {
164953
165098
  const timeoutMs = 3e4;
164954
165099
  const controller = new AbortController();
@@ -164967,7 +165112,7 @@ async function fetchRunContext(params) {
164967
165112
  });
164968
165113
  clearTimeout(timeoutId);
164969
165114
  if (!response.ok) {
164970
- return defaultRunContext;
165115
+ return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
164971
165116
  }
164972
165117
  const data = await response.json();
164973
165118
  if (data === null) {
@@ -164991,11 +165136,12 @@ async function fetchRunContext(params) {
164991
165136
  oss: data.oss ?? false,
164992
165137
  plan: data.plan ?? "none",
164993
165138
  proxyModel: data.proxyModel,
164994
- dbSecrets: data.dbSecrets
165139
+ dbSecrets: data.dbSecrets,
165140
+ secretsUnavailable: data.secretsUnavailable
164995
165141
  };
164996
165142
  } catch {
164997
165143
  clearTimeout(timeoutId);
164998
- return defaultRunContext;
165144
+ return unknownSecretsRunContext;
164999
165145
  }
165000
165146
  }
165001
165147
 
@@ -165017,7 +165163,10 @@ async function resolveRunContextData(params) {
165017
165163
  const repoContext = parseRepoContext();
165018
165164
  let oidcToken;
165019
165165
  try {
165020
- oidcToken = await core9.getIDToken("pullfrog-api");
165166
+ oidcToken = await op(() => core9.getIDToken("pullfrog-api"), {
165167
+ name: "OIDC mint",
165168
+ retries: process.env.ACTIONS_ID_TOKEN_REQUEST_URL ? [200, 1e3] : []
165169
+ })();
165021
165170
  } catch {
165022
165171
  }
165023
165172
  const [repoResponse, runContext] = await Promise.all([
@@ -165039,7 +165188,10 @@ async function resolveRunContextData(params) {
165039
165188
  oss: runContext.oss,
165040
165189
  plan: runContext.plan,
165041
165190
  proxyModel: runContext.proxyModel,
165042
- dbSecrets: runContext.dbSecrets
165191
+ dbSecrets: runContext.dbSecrets,
165192
+ // a failed mint on a runner that should have been able to mint is the same
165193
+ // outcome as the server-side failure: the run never sees stored secrets.
165194
+ secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0
165043
165195
  };
165044
165196
  }
165045
165197
 
@@ -165204,6 +165356,9 @@ function renderRunError(input) {
165204
165356
  if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
165205
165357
  return { summary: input.errorMessage, comment: input.errorMessage };
165206
165358
  }
165359
+ if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
165360
+ return { summary: input.errorMessage, comment: input.errorMessage };
165361
+ }
165207
165362
  const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
165208
165363
  const hangBody = isHang ? formatAgentHangBody({
165209
165364
  diagnostic: input.agentDiagnostic,
@@ -165938,7 +166093,8 @@ async function main() {
165938
166093
  model: effectiveModel,
165939
166094
  authorized: getAuthorizedModels(),
165940
166095
  owner: runContext.repo.owner,
165941
- name: runContext.repo.name
166096
+ name: runContext.repo.name,
166097
+ secretsUnavailable: runContext.secretsUnavailable
165942
166098
  });
165943
166099
  }
165944
166100
  await setupGit({
package/dist/internal.js CHANGED
@@ -381,6 +381,28 @@ var providers = {
381
381
  }
382
382
  }
383
383
  }),
384
+ "openai-compatible": provider({
385
+ // "Custom" is the picker group, "OpenAI-compatible" the entry under it, so the
386
+ // menu reads `Custom › OpenAI-compatible` and a second custom backend (a
387
+ // different wire format, say) slots in beside it without a rename. the
388
+ // provider KEY stays `openai-compatible` — it's the stored slug and the
389
+ // `OPENAI_COMPATIBLE_*` env prefix, so this is display-only.
390
+ displayName: "Custom",
391
+ // bring-your-own generic OpenAI-compatible endpoint — Cloudflare AI Gateway,
392
+ // Alibaba DashScope, self-hosted vLLM, or any compatible gateway. base URL +
393
+ // key + model ID are all supplied via env; nothing is cataloged or bumped.
394
+ envVars: ["OPENAI_COMPATIBLE_BASE_URL", "OPENAI_COMPATIBLE_API_KEY", "OPENAI_COMPATIBLE_MODEL"],
395
+ models: {
396
+ // single routing entry — the actual model ID is read from
397
+ // OPENAI_COMPATIBLE_MODEL at run time and the provider is materialized
398
+ // via `@ai-sdk/openai-compatible`.
399
+ byok: {
400
+ displayName: "OpenAI-compatible",
401
+ resolve: "openai-compatible",
402
+ routing: "openai-compatible"
403
+ }
404
+ }
405
+ }),
384
406
  openrouter: provider({
385
407
  displayName: "OpenRouter",
386
408
  envVars: ["OPENROUTER_API_KEY"],
package/dist/models.d.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * env var and routes to claude-code for Anthropic IDs or opencode for
21
21
  * everything else.
22
22
  */
23
- export type ModelRouting = "bedrock" | "vertex";
23
+ export type ModelRouting = "bedrock" | "vertex" | "openai-compatible";
24
24
  export interface ModelAlias {
25
25
  /** stable alias stored in DB, e.g. "anthropic/claude-opus" */
26
26
  slug: string;
@@ -99,6 +99,7 @@ export declare const providers: {
99
99
  "opencode-go": ProviderConfig;
100
100
  bedrock: ProviderConfig;
101
101
  vertex: ProviderConfig;
102
+ "openai-compatible": ProviderConfig;
102
103
  openrouter: ProviderConfig;
103
104
  };
104
105
  export type ModelProvider = keyof typeof providers;
@@ -196,6 +197,31 @@ export declare function getAutoSelectHintModel(): string;
196
197
  export declare const BEDROCK_MODEL_ID_ENV = "BEDROCK_MODEL_ID";
197
198
  /** env var that supplies the Vertex AI model ID for the `vertex/byok` slug. */
198
199
  export declare const VERTEX_MODEL_ID_ENV = "VERTEX_MODEL_ID";
200
+ /** provider key + slug prefix for the generic OpenAI-compatible BYOK backend. */
201
+ export declare const OPENAI_COMPATIBLE_PROVIDER = "openai-compatible";
202
+ /** base URL of the user's OpenAI-compatible endpoint (e.g. a Cloudflare AI Gateway URL). */
203
+ export declare const OPENAI_COMPATIBLE_BASE_URL_ENV = "OPENAI_COMPATIBLE_BASE_URL";
204
+ /** API key/token for the user's OpenAI-compatible endpoint — the one sensitive secret. */
205
+ export declare const OPENAI_COMPATIBLE_API_KEY_ENV = "OPENAI_COMPATIBLE_API_KEY";
206
+ /** model ID served by the endpoint, supplied for the `openai-compatible/byok` slug. */
207
+ export declare const OPENAI_COMPATIBLE_MODEL_ENV = "OPENAI_COMPATIBLE_MODEL";
208
+ /**
209
+ * context-window size of the endpoint's model. required — `validateOpenAICompatibleSetup`
210
+ * rejects the run pre-agent when it's unset or non-numeric. it also gates
211
+ * auto-compaction: opencode's `isOverflow` short-circuits when
212
+ * `limit.context === 0`, which would otherwise let a long session grow until the
213
+ * endpoint rejects it on context length.
214
+ */
215
+ export declare const OPENAI_COMPATIBLE_CONTEXT_ENV = "OPENAI_COMPATIBLE_CONTEXT";
216
+ /**
217
+ * max completion tokens the endpoint's model accepts. required — see
218
+ * OPENAI_COMPATIBLE_CONTEXT_ENV. opencode has no models.dev metadata for a
219
+ * user-supplied endpoint, and an undeclared limit makes it send
220
+ * `max_tokens: 32000`, which most models reject outright (gpt-4o and gpt-4o-mini
221
+ * cap at 16384, many open models at 4096/8192). opencode's `limit` requires
222
+ * `context` + `output` together, so the pair is validated and emitted as a unit.
223
+ */
224
+ export declare const OPENAI_COMPATIBLE_MAX_OUTPUT_ENV = "OPENAI_COMPATIBLE_MAX_OUTPUT";
199
225
  /**
200
226
  * the Bedrock model ID passed to claude-code or opencode is whatever the
201
227
  * user set in `BEDROCK_MODEL_ID` — Pullfrog never resolves or upgrades it.
@@ -1,3 +1,10 @@
1
+ /**
2
+ * marker for the distinct "run-context couldn't hand over your stored secrets"
3
+ * body. surfaced verbatim by `runErrorRenderer` (same contract as
4
+ * `MODEL_ACCESS_MARKER`) because blaming the user for a transient fetch failure
5
+ * on our side is the wrong CTA — their key is configured and still stored.
6
+ */
7
+ export declare const SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
1
8
  /**
2
9
  * Validate that the resolved model can actually be served by the chosen
3
10
  * agent. For routing slugs (Bedrock / Vertex) the auth shape is multi-var
@@ -16,6 +23,9 @@ export declare function validateAgentApiKey(params: {
16
23
  authorized: Set<string>;
17
24
  owner: string;
18
25
  name: string;
26
+ /** run-context couldn't hand over Pullfrog-stored secrets, so a missing key
27
+ * says nothing about what the user actually configured. */
28
+ secretsUnavailable?: boolean | undefined;
19
29
  }): void;
20
30
  /**
21
31
  * Detect agent-runtime auth failures that should be reformatted as an actionable
@@ -55,6 +55,13 @@ export interface RunContext {
55
55
  plan: AccountPlan;
56
56
  proxyModel?: string | undefined;
57
57
  dbSecrets?: Record<string, string> | undefined;
58
+ /**
59
+ * the server tried and failed to materialize Pullfrog-stored secrets (or we
60
+ * never got a usable response at all). distinct from an absent `dbSecrets`,
61
+ * which legitimately means the user has none stored — without the
62
+ * distinction a transient failure renders as "you have no API key".
63
+ */
64
+ secretsUnavailable?: boolean | undefined;
58
65
  }
59
66
  /**
60
67
  * fetch run context from Pullfrog API
@@ -13,6 +13,9 @@ export interface RunContextData {
13
13
  plan: AccountPlan;
14
14
  proxyModel?: string | undefined;
15
15
  dbSecrets?: Record<string, string> | undefined;
16
+ /** stored secrets couldn't be materialized for this run — not the same as
17
+ * the user having none. see `RunContext.secretsUnavailable`. */
18
+ secretsUnavailable?: boolean | undefined;
16
19
  }
17
20
  interface ResolveRunContextDataParams {
18
21
  octokit: OctokitWithPlugins;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",