pullfrog 0.1.44 → 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.44",
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) {
@@ -166572,6 +166713,7 @@ async function mintProxyKey(ctx) {
166572
166713
  }
166573
166714
  }
166574
166715
  async function buildProxyTokenHeaders(ctx) {
166716
+ const fundingSource = ctx.oss ? "oss" : "router";
166575
166717
  if (ctx.oidcCredentials) {
166576
166718
  const creds = ctx.oidcCredentials;
166577
166719
  const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
@@ -166579,11 +166721,17 @@ async function buildProxyTokenHeaders(ctx) {
166579
166721
  retries: [1e3, 2e3],
166580
166722
  bail: (error49) => !isTransientTokenError(error49)
166581
166723
  })();
166582
- return { Authorization: `Bearer ${oidcToken}` };
166724
+ return {
166725
+ Authorization: `Bearer ${oidcToken}`,
166726
+ "X-Pullfrog-Funding-Source": fundingSource
166727
+ };
166583
166728
  }
166584
166729
  if (isLocalApiUrl()) {
166585
166730
  log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
166586
- return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
166731
+ return {
166732
+ "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}`,
166733
+ "X-Pullfrog-Funding-Source": fundingSource
166734
+ };
166587
166735
  }
166588
166736
  return null;
166589
166737
  }
@@ -166594,7 +166742,11 @@ async function resolveProxyModel(ctx) {
166594
166742
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
166595
166743
  return;
166596
166744
  }
166597
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
166745
+ const key = await mintProxyKey({
166746
+ oidcCredentials: ctx.oidcCredentials,
166747
+ repo: ctx.repo,
166748
+ oss: ctx.oss
166749
+ });
166598
166750
  if (!key) return;
166599
166751
  process.env.OPENROUTER_API_KEY = key;
166600
166752
  core8.setSecret(key);
@@ -166825,6 +166977,10 @@ var defaultRunContext = {
166825
166977
  oss: false,
166826
166978
  plan: "none"
166827
166979
  };
166980
+ var unknownSecretsRunContext = {
166981
+ ...defaultRunContext,
166982
+ secretsUnavailable: true
166983
+ };
166828
166984
  async function fetchRunContext(params) {
166829
166985
  const timeoutMs = 3e4;
166830
166986
  const controller = new AbortController();
@@ -166843,7 +166999,7 @@ async function fetchRunContext(params) {
166843
166999
  });
166844
167000
  clearTimeout(timeoutId);
166845
167001
  if (!response.ok) {
166846
- return defaultRunContext;
167002
+ return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
166847
167003
  }
166848
167004
  const data = await response.json();
166849
167005
  if (data === null) {
@@ -166867,11 +167023,12 @@ async function fetchRunContext(params) {
166867
167023
  oss: data.oss ?? false,
166868
167024
  plan: data.plan ?? "none",
166869
167025
  proxyModel: data.proxyModel,
166870
- dbSecrets: data.dbSecrets
167026
+ dbSecrets: data.dbSecrets,
167027
+ secretsUnavailable: data.secretsUnavailable
166871
167028
  };
166872
167029
  } catch {
166873
167030
  clearTimeout(timeoutId);
166874
- return defaultRunContext;
167031
+ return unknownSecretsRunContext;
166875
167032
  }
166876
167033
  }
166877
167034
 
@@ -166893,7 +167050,10 @@ async function resolveRunContextData(params) {
166893
167050
  const repoContext = parseRepoContext();
166894
167051
  let oidcToken;
166895
167052
  try {
166896
- 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
+ })();
166897
167057
  } catch {
166898
167058
  }
166899
167059
  const [repoResponse, runContext] = await Promise.all([
@@ -166915,7 +167075,10 @@ async function resolveRunContextData(params) {
166915
167075
  oss: runContext.oss,
166916
167076
  plan: runContext.plan,
166917
167077
  proxyModel: runContext.proxyModel,
166918
- 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
166919
167082
  };
166920
167083
  }
166921
167084
 
@@ -167080,6 +167243,9 @@ function renderRunError(input) {
167080
167243
  if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
167081
167244
  return { summary: input.errorMessage, comment: input.errorMessage };
167082
167245
  }
167246
+ if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
167247
+ return { summary: input.errorMessage, comment: input.errorMessage };
167248
+ }
167083
167249
  const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
167084
167250
  const hangBody = isHang ? formatAgentHangBody({
167085
167251
  diagnostic: input.agentDiagnostic,
@@ -167814,7 +167980,8 @@ async function main() {
167814
167980
  model: effectiveModel,
167815
167981
  authorized: getAuthorizedModels(),
167816
167982
  owner: runContext.repo.owner,
167817
- name: runContext.repo.name
167983
+ name: runContext.repo.name,
167984
+ secretsUnavailable: runContext.secretsUnavailable
167818
167985
  });
167819
167986
  }
167820
167987
  await setupGit({
@@ -168311,7 +168478,7 @@ function link(text, url4) {
168311
168478
  return `\x1B]8;;${url4}\x07${text}\x1B]8;;\x07`;
168312
168479
  }
168313
168480
  function buildProviders() {
168314
- 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]) => {
168315
168482
  const aliases = modelAliases.filter(
168316
168483
  (a) => a.provider === key && !a.fallback && !a.routing && !a.hidden
168317
168484
  );
@@ -168331,7 +168498,7 @@ function buildProviders() {
168331
168498
  hint: a === recommended ? "recommended" : void 0
168332
168499
  }))
168333
168500
  };
168334
- });
168501
+ }).filter((p2) => p2.models.length > 0);
168335
168502
  }
168336
168503
  var CLI_PROVIDERS = buildProviders();
168337
168504
  function resolveModelProvider(slug2) {
@@ -169141,7 +169308,7 @@ async function runCli4(input) {
169141
169308
  }
169142
169309
 
169143
169310
  // cli.ts
169144
- var VERSION10 = "0.1.44";
169311
+ var VERSION10 = "0.1.46";
169145
169312
  var bin = basename2(process.argv[1] || "");
169146
169313
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
169147
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.44",
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) {
@@ -164685,6 +164826,7 @@ async function mintProxyKey(ctx) {
164685
164826
  }
164686
164827
  }
164687
164828
  async function buildProxyTokenHeaders(ctx) {
164829
+ const fundingSource = ctx.oss ? "oss" : "router";
164688
164830
  if (ctx.oidcCredentials) {
164689
164831
  const creds = ctx.oidcCredentials;
164690
164832
  const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
@@ -164692,11 +164834,17 @@ async function buildProxyTokenHeaders(ctx) {
164692
164834
  retries: [1e3, 2e3],
164693
164835
  bail: (error49) => !isTransientTokenError(error49)
164694
164836
  })();
164695
- return { Authorization: `Bearer ${oidcToken}` };
164837
+ return {
164838
+ Authorization: `Bearer ${oidcToken}`,
164839
+ "X-Pullfrog-Funding-Source": fundingSource
164840
+ };
164696
164841
  }
164697
164842
  if (isLocalApiUrl()) {
164698
164843
  log.info(`\xBB proxy: dev bypass (x-dev-repo) for ${ctx.repo.owner}/${ctx.repo.name}`);
164699
- return { "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}` };
164844
+ return {
164845
+ "x-dev-repo": `${ctx.repo.owner}/${ctx.repo.name}`,
164846
+ "X-Pullfrog-Funding-Source": fundingSource
164847
+ };
164700
164848
  }
164701
164849
  return null;
164702
164850
  }
@@ -164707,7 +164855,11 @@ async function resolveProxyModel(ctx) {
164707
164855
  log.warning("\xBB proxy requested but no OIDC credentials available \u2014 skipping");
164708
164856
  return;
164709
164857
  }
164710
- const key = await mintProxyKey({ oidcCredentials: ctx.oidcCredentials, repo: ctx.repo });
164858
+ const key = await mintProxyKey({
164859
+ oidcCredentials: ctx.oidcCredentials,
164860
+ repo: ctx.repo,
164861
+ oss: ctx.oss
164862
+ });
164711
164863
  if (!key) return;
164712
164864
  process.env.OPENROUTER_API_KEY = key;
164713
164865
  core8.setSecret(key);
@@ -164938,6 +165090,10 @@ var defaultRunContext = {
164938
165090
  oss: false,
164939
165091
  plan: "none"
164940
165092
  };
165093
+ var unknownSecretsRunContext = {
165094
+ ...defaultRunContext,
165095
+ secretsUnavailable: true
165096
+ };
164941
165097
  async function fetchRunContext(params) {
164942
165098
  const timeoutMs = 3e4;
164943
165099
  const controller = new AbortController();
@@ -164956,7 +165112,7 @@ async function fetchRunContext(params) {
164956
165112
  });
164957
165113
  clearTimeout(timeoutId);
164958
165114
  if (!response.ok) {
164959
- return defaultRunContext;
165115
+ return response.status >= 500 ? unknownSecretsRunContext : defaultRunContext;
164960
165116
  }
164961
165117
  const data = await response.json();
164962
165118
  if (data === null) {
@@ -164980,11 +165136,12 @@ async function fetchRunContext(params) {
164980
165136
  oss: data.oss ?? false,
164981
165137
  plan: data.plan ?? "none",
164982
165138
  proxyModel: data.proxyModel,
164983
- dbSecrets: data.dbSecrets
165139
+ dbSecrets: data.dbSecrets,
165140
+ secretsUnavailable: data.secretsUnavailable
164984
165141
  };
164985
165142
  } catch {
164986
165143
  clearTimeout(timeoutId);
164987
- return defaultRunContext;
165144
+ return unknownSecretsRunContext;
164988
165145
  }
164989
165146
  }
164990
165147
 
@@ -165006,7 +165163,10 @@ async function resolveRunContextData(params) {
165006
165163
  const repoContext = parseRepoContext();
165007
165164
  let oidcToken;
165008
165165
  try {
165009
- 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
+ })();
165010
165170
  } catch {
165011
165171
  }
165012
165172
  const [repoResponse, runContext] = await Promise.all([
@@ -165028,7 +165188,10 @@ async function resolveRunContextData(params) {
165028
165188
  oss: runContext.oss,
165029
165189
  plan: runContext.plan,
165030
165190
  proxyModel: runContext.proxyModel,
165031
- 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
165032
165195
  };
165033
165196
  }
165034
165197
 
@@ -165193,6 +165356,9 @@ function renderRunError(input) {
165193
165356
  if (input.errorMessage.includes(MODEL_ACCESS_MARKER)) {
165194
165357
  return { summary: input.errorMessage, comment: input.errorMessage };
165195
165358
  }
165359
+ if (input.errorMessage.includes(SECRETS_UNAVAILABLE_MARKER)) {
165360
+ return { summary: input.errorMessage, comment: input.errorMessage };
165361
+ }
165196
165362
  const isHang = input.errorMessage.startsWith("activity timeout") || input.errorMessage.startsWith("agent still pending");
165197
165363
  const hangBody = isHang ? formatAgentHangBody({
165198
165364
  diagnostic: input.agentDiagnostic,
@@ -165927,7 +166093,8 @@ async function main() {
165927
166093
  model: effectiveModel,
165928
166094
  authorized: getAuthorizedModels(),
165929
166095
  owner: runContext.repo.owner,
165930
- name: runContext.repo.name
166096
+ name: runContext.repo.name,
166097
+ secretsUnavailable: runContext.secretsUnavailable
165931
166098
  });
165932
166099
  }
165933
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.44",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",