pullfrog 0.1.66 → 0.1.68

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.
package/dist/index.js CHANGED
@@ -107718,6 +107718,7 @@ import { performance as performance5 } from "node:perf_hooks";
107718
107718
 
107719
107719
  // effort.ts
107720
107720
  var DEFAULT_EFFORT_POSITION = 0.5;
107721
+ var SUBSIDY_RUNG = "high";
107721
107722
  var EFFORT_ALIASES = {
107722
107723
  low: 0,
107723
107724
  medium: 0.25,
@@ -108042,6 +108043,69 @@ var providers = {
108042
108043
  }
108043
108044
  }
108044
108045
  }),
108046
+ // the same models as `moonshotai` above, billed against a Kimi membership
108047
+ // instead of a Moonshot API balance — a separate endpoint, separate model
108048
+ // ids and a separate key, which is why it is a provider rather than a second
108049
+ // `envVars` entry. no provider injection is needed: models.dev carries
108050
+ // `kimi-for-coding` (api `https://api.kimi.com/coding/v1`, npm
108051
+ // `@ai-sdk/anthropic`) and opencode's env loop enables it from a bare
108052
+ // `KIMI_API_KEY`, verified against our pinned opencode-ai@1.18.5 — all four
108053
+ // ids list in `opencode models` and a run reaches Kimi's own auth check.
108054
+ //
108055
+ // NO `openRouterResolve` ON PURPOSE. OpenRouter cannot serve a Kimi
108056
+ // membership, so a Router path here would silently bill the wallet for
108057
+ // Moonshot pay-as-you-go under a slug picked for the opposite reason. The
108058
+ // absence is load-bearing: `isCardGatedModel` reads it as "not Router-
108059
+ // resolvable", `decideModelAccess` answers an explicit pick with the
108060
+ // `router` copy ("add your own provider key"), and run-context clamps a
108061
+ // keyless Router pick to the default subsidy model with `noRouterPath`.
108062
+ "kimi-for-coding": provider({
108063
+ // "Kimi Code" is the product name on kimi.com/code and in the console the
108064
+ // key is minted from. The provider KEY has to stay `kimi-for-coding` — it
108065
+ // is models.dev's id, so it is what `enabled_providers` and every
108066
+ // `resolve` prefix must spell.
108067
+ displayName: "Kimi Code",
108068
+ envVars: ["KIMI_API_KEY"],
108069
+ models: {
108070
+ // the tier each model needs is in `description` because Kimi answers an
108071
+ // out-of-tier request with 401 — indistinguishable from a bad key by
108072
+ // status alone, and only `isKimiPlanTierError` tells them apart after the
108073
+ // fact. saying it in the picker is the half that stops the run happening.
108074
+ "kimi-k3": {
108075
+ displayName: "Kimi K3",
108076
+ description: "1M context, but only on Allegretto and above \xB7 Moderato caps it at 256K",
108077
+ resolve: "kimi-for-coding/k3",
108078
+ effort: ["low", "high", "max"],
108079
+ subagentModel: "kimi-k2"
108080
+ },
108081
+ // `preferred` sits on the 256K variant rather than the 1M flagship, which
108082
+ // inverts the usual "top tier wins" rule for a reason specific to Kimi:
108083
+ // below Allegretto the two are the SAME 256K ceiling and `k3` charges
108084
+ // roughly double the quota for it, so on the median tier the flagship is
108085
+ // strictly dominated rather than merely pricier. This flag seeds the
108086
+ // onboarding pick (`getRecommendedSlug`) and wins key-only auto-select
108087
+ // (`autoSelectModel`), which are exactly the two moments nobody has
108088
+ // chosen yet.
108089
+ "kimi-k3-256k": {
108090
+ displayName: "Kimi K3 256K",
108091
+ description: "Same model at 256K for ~half the quota \xB7 Moderato or above",
108092
+ resolve: "kimi-for-coding/k3-256k",
108093
+ effort: ["low", "high", "max"],
108094
+ preferred: true,
108095
+ subagentModel: "kimi-k2"
108096
+ },
108097
+ "kimi-k2": {
108098
+ displayName: "Kimi K2",
108099
+ description: "Included in every Kimi membership tier",
108100
+ resolve: "kimi-for-coding/kimi-for-coding"
108101
+ },
108102
+ "kimi-k2-highspeed": {
108103
+ displayName: "Kimi K2 HighSpeed",
108104
+ description: "~6x output speed for ~3x quota \xB7 needs Allegretto or above",
108105
+ resolve: "kimi-for-coding/kimi-for-coding-highspeed"
108106
+ }
108107
+ }
108108
+ }),
108045
108109
  opencode: provider({
108046
108110
  displayName: "OpenCode Zen",
108047
108111
  envVars: ["OPENCODE_API_KEY"],
@@ -108058,6 +108122,15 @@ var providers = {
108058
108122
  // the model does cost nothing.
108059
108123
  isFree: true
108060
108124
  },
108125
+ // Zen meters Fable like any other model, so this route reaches it without
108126
+ // the Anthropic access grant `anthropic/claude-fable` still needs.
108127
+ "claude-fable": {
108128
+ displayName: "Claude Fable",
108129
+ resolve: "opencode/claude-fable-5-1",
108130
+ effort: ["low", "medium", "high", "xhigh", "max"],
108131
+ openRouterResolve: "openrouter/~anthropic/claude-fable-latest",
108132
+ subagentModel: "claude-sonnet"
108133
+ },
108061
108134
  "claude-opus": {
108062
108135
  displayName: "Claude Opus",
108063
108136
  resolve: "opencode/claude-opus-5",
@@ -108171,6 +108244,17 @@ var providers = {
108171
108244
  effort: ["minimal", "low", "medium", "high"],
108172
108245
  openRouterResolve: "openrouter/google/gemini-3.6-flash"
108173
108246
  },
108247
+ // Zen serves K3, but rule 7 can only move a mirror when its upstream moves,
108248
+ // so the K3 generation could never reach this block by bumping `kimi-k2`.
108249
+ "kimi-k3": {
108250
+ displayName: "Kimi K3",
108251
+ resolve: "opencode/kimi-k3",
108252
+ // Zen publishes a single rung for K3 where OpenRouter publishes three.
108253
+ effort: ["max"],
108254
+ openRouterEffort: ["low", "high", "max"],
108255
+ openRouterResolve: "openrouter/moonshotai/kimi-k3",
108256
+ subagentModel: "kimi-k2"
108257
+ },
108174
108258
  "kimi-k2": {
108175
108259
  displayName: "Kimi K2",
108176
108260
  // k2.7-code is UNDEPLOYED on Zen — still listed in /zen/v1/models, but
@@ -108182,25 +108266,58 @@ var providers = {
108182
108266
  resolve: "opencode/kimi-k2.6",
108183
108267
  openRouterResolve: "openrouter/moonshotai/kimi-k2.7-code"
108184
108268
  },
108269
+ // M3 is its own line beside M2, not a bump of it — same reason `kimi-k3`
108270
+ // sits beside `kimi-k2` rather than replacing it.
108271
+ "minimax-m3": {
108272
+ displayName: "MiniMax M3",
108273
+ resolve: "opencode/minimax-m3",
108274
+ openRouterResolve: "openrouter/minimax/minimax-m3"
108275
+ },
108185
108276
  // slug pins the m2 line for DB stability; resolve tracks the current m2.7.
108186
108277
  "minimax-m2.5": {
108187
108278
  displayName: "MiniMax M2",
108188
108279
  resolve: "opencode/minimax-m2.7",
108189
108280
  openRouterResolve: "openrouter/minimax/minimax-m2.7"
108190
108281
  },
108282
+ // Z.ai and xAI reach Zen subscribers only here — before this, `opencode-go`
108283
+ // was the catalog's only GLM route, and Grok had no Zen route at all.
108284
+ glm: {
108285
+ displayName: "GLM",
108286
+ resolve: "opencode/glm-5.2",
108287
+ effort: ["high", "max"],
108288
+ openRouterEffort: ["high", "xhigh"],
108289
+ openRouterResolve: "openrouter/z-ai/glm-5.2"
108290
+ },
108291
+ grok: {
108292
+ displayName: "Grok",
108293
+ resolve: "opencode/grok-4.6",
108294
+ effort: ["low", "medium", "high", "xhigh"],
108295
+ openRouterResolve: "openrouter/x-ai/grok-4.6"
108296
+ },
108191
108297
  "gpt-5-nano": {
108192
108298
  displayName: "GPT Nano",
108193
108299
  resolve: "opencode/gpt-5-nano",
108194
108300
  effort: ["minimal", "low", "medium", "high"],
108195
108301
  openRouterResolve: "openrouter/openai/gpt-5-nano"
108196
108302
  },
108303
+ // Zen's live free MiMo, and the second free row in a menu that big-pickle
108304
+ // was alone in since `mimo-v2-pro-free` lost its model.
108305
+ mimo: {
108306
+ displayName: "MiMo",
108307
+ resolve: "opencode/mimo-v2.5-free",
108308
+ // free to run, still gated on the provider's own OPENCODE_API_KEY —
108309
+ // see the big-pickle note above (#1077).
108310
+ isFree: true
108311
+ },
108197
108312
  "mimo-v2-pro-free": {
108198
108313
  displayName: "MiMo V2 Pro",
108199
108314
  resolve: "opencode/mimo-v2-pro-free",
108200
108315
  // free to run, still gated on the provider's own OPENCODE_API_KEY —
108201
108316
  // see the big-pickle note above (#1077).
108202
108317
  isFree: true,
108203
- fallback: "opencode/big-pickle"
108318
+ // the model id Zen dropped; land a stored MiMo pick back on MiMo rather
108319
+ // than on the unrelated model it had to settle for while none was live.
108320
+ fallback: "opencode/mimo"
108204
108321
  },
108205
108322
  "minimax-m2.5-free": {
108206
108323
  displayName: "MiniMax M2",
@@ -108295,6 +108412,16 @@ var providers = {
108295
108412
  resolve: "opencode-go/qwen3.7-plus",
108296
108413
  openRouterResolve: "openrouter/qwen/qwen3.7-plus"
108297
108414
  },
108415
+ // the cheap rung Alibaba added under Plus (0.15/0.47 against 0.5/3), and
108416
+ // the only model either OpenCode plan has added since this block was written.
108417
+ "qwen-flash": {
108418
+ displayName: "Qwen Flash",
108419
+ resolve: "opencode-go/qwen3.8-flash",
108420
+ effort: ["low", "medium", "xhigh"],
108421
+ // the Go route publishes rungs where OpenRouter publishes none.
108422
+ openRouterEffort: [],
108423
+ openRouterResolve: "openrouter/qwen/qwen3.8-flash"
108424
+ },
108298
108425
  // MiniMax — parity with opencode/* and openrouter/*; the m2 slug pins the
108299
108426
  // line for DB stability while the resolve tracks the current m2.7.
108300
108427
  "minimax-m3": {
@@ -108594,6 +108721,80 @@ var providers = {
108594
108721
  openRouterResolve: "openrouter/minimax/minimax-m3"
108595
108722
  }
108596
108723
  }
108724
+ }),
108725
+ vercel: provider({
108726
+ // Vercel AI Gateway — one key serves every model it fronts. model IDs use
108727
+ // the models.dev `vercel` catalog's own naming (dotted versions, e.g.
108728
+ // `anthropic/claude-opus-5`, `zai/glm-5.3`), and effort ladders mirror that
108729
+ // catalog's entries, not the upstream vendor's. deliberately no
108730
+ // `openRouterResolve`: a gateway pick is BYOK-only — silently rerouting it
108731
+ // over the Router proxy would bill the wallet for traffic the user pointed
108732
+ // at their own gateway.
108733
+ displayName: "Vercel AI Gateway",
108734
+ envVars: ["AI_GATEWAY_API_KEY"],
108735
+ models: {
108736
+ "claude-opus": {
108737
+ displayName: "Claude Opus",
108738
+ resolve: "vercel/anthropic/claude-opus-5",
108739
+ effort: ["low", "medium", "high", "xhigh", "max"],
108740
+ preferred: true,
108741
+ subagentModel: "claude-sonnet"
108742
+ },
108743
+ "claude-sonnet": {
108744
+ displayName: "Claude Sonnet",
108745
+ resolve: "vercel/anthropic/claude-sonnet-5",
108746
+ effort: ["low", "medium", "high", "xhigh"]
108747
+ },
108748
+ "claude-haiku": {
108749
+ displayName: "Claude Haiku",
108750
+ resolve: "vercel/anthropic/claude-haiku-4.5"
108751
+ },
108752
+ "gpt-sol": {
108753
+ displayName: "GPT Sol",
108754
+ resolve: "vercel/openai/gpt-5.6-sol",
108755
+ effort: ["none", "low", "medium", "high", "xhigh", "max"],
108756
+ subagentModel: "gpt-terra"
108757
+ },
108758
+ "gpt-terra": {
108759
+ displayName: "GPT Terra",
108760
+ resolve: "vercel/openai/gpt-5.6-terra",
108761
+ effort: ["none", "low", "medium", "high", "xhigh", "max"]
108762
+ },
108763
+ "gpt-luna": {
108764
+ displayName: "GPT Luna",
108765
+ resolve: "vercel/openai/gpt-5.6-luna",
108766
+ effort: ["none", "low", "medium", "high", "xhigh", "max"]
108767
+ },
108768
+ "gemini-pro": {
108769
+ displayName: "Gemini Pro",
108770
+ resolve: "vercel/google/gemini-3.1-pro-preview",
108771
+ effort: ["low", "medium", "high"]
108772
+ },
108773
+ "gemini-flash": {
108774
+ displayName: "Gemini Flash",
108775
+ resolve: "vercel/google/gemini-3.6-flash",
108776
+ effort: ["minimal", "low", "medium", "high"]
108777
+ },
108778
+ "deepseek-pro": {
108779
+ displayName: "DeepSeek Pro",
108780
+ resolve: "vercel/deepseek/deepseek-v4-pro-0813",
108781
+ effort: ["high", "xhigh"]
108782
+ },
108783
+ "deepseek-flash": {
108784
+ displayName: "DeepSeek Flash",
108785
+ resolve: "vercel/deepseek/deepseek-v4-flash",
108786
+ effort: ["high", "xhigh"]
108787
+ },
108788
+ glm: {
108789
+ displayName: "GLM",
108790
+ resolve: "vercel/zai/glm-5.3",
108791
+ effort: ["low", "high", "max"]
108792
+ },
108793
+ "kimi-k3": {
108794
+ displayName: "Kimi K3",
108795
+ resolve: "vercel/moonshotai/kimi-k3"
108796
+ }
108797
+ }
108597
108798
  })
108598
108799
  };
108599
108800
  function parseModel(slug2) {
@@ -109345,6 +109546,7 @@ async function installFromNpmTarball(params) {
109345
109546
  // utils/providerErrors.ts
109346
109547
  var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
109347
109548
  var PROVIDER_NO_ENDPOINTS_LABEL = "provider no routable endpoints";
109549
+ var PROVIDER_USAGE_LIMIT_LABEL = "provider usage limit reached";
109348
109550
  var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
109349
109551
  var ANTHROPIC_SPEND_CAP_PATTERN = /reached your specified API usage limits\.?(?:\s*You will regain access on ([^"\n]+?)\.)?/i;
109350
109552
  function findAnthropicSpendCap(text) {
@@ -109390,6 +109592,14 @@ var PROVIDER_ERROR_PATTERNS = [
109390
109592
  // the wallet-shaped patterns above reach, and its 400 sits nowhere near a
109391
109593
  // `status:` key either — so the whole message was dropped on the floor (#1208).
109392
109594
  { regex: ANTHROPIC_SPEND_CAP_PATTERN, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
109595
+ // OpenCode Zen / Codex-subscription cap: `AI_APICallError: The usage limit has
109596
+ // been reached`. Its noun is "usage limit" with no "rate", no status code and
109597
+ // no wallet wording, so every pattern here missed it and `lastProviderError`
109598
+ // stayed unset — which suppresses nothing, so the run was reported as "The
109599
+ // model produced no output at all … usually transient; re-running often
109600
+ // succeeds" while the same log held nine of these. Measured on a live account
109601
+ // whose ChatGPT subscription was capped.
109602
+ { regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
109393
109603
  // the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
109394
109604
  // run those mean the PULLFROG wallet is empty and `renderRunError` returns a
109395
109605
  // `BillingError` before ever reaching this list; on a BYOK run the very same
@@ -109520,7 +109730,7 @@ function ossEffortFloor(ctx) {
109520
109730
  const alias = resolveRunAlias(ctx);
109521
109731
  const published = alias?.openRouterEffort ?? alias?.effort;
109522
109732
  if (!published) return 0;
109523
- return rungPosition({ rung: "high", published }) ?? 0;
109733
+ return rungPosition({ rung: SUBSIDY_RUNG, published }) ?? 0;
109524
109734
  }
109525
109735
  function resolveRunEffort(ctx) {
109526
109736
  const configured = ctx.payload.effort !== void 0;
@@ -109552,7 +109762,7 @@ var import_semver = __toESM(require_semver2(), 1);
109552
109762
  // package.json
109553
109763
  var package_default = {
109554
109764
  name: "pullfrog",
109555
- version: "0.1.66",
109765
+ version: "0.1.68",
109556
109766
  type: "module",
109557
109767
  bin: {
109558
109768
  pullfrog: "dist/cli.mjs",
@@ -192236,6 +192446,9 @@ function formatModelLabel(params) {
192236
192446
  if (params.fallbackFrom) {
192237
192447
  return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
192238
192448
  }
192449
+ if (params.clamped?.reason === "trial") {
192450
+ return `${base} (free trial)`;
192451
+ }
192239
192452
  if (params.clamped) {
192240
192453
  const target = isAutoTier(params.clamped.from) ? "the intelligent tier" : `\`${resolveDisplayAlias(params.clamped.from)?.displayName ?? params.clamped.from}\``;
192241
192454
  return params.clamped.reason === "card" ? `${base} (${target} needs a [card on file](https://docs.pullfrog.com/models))` : `${base} (${target} needs a [provider key](https://docs.pullfrog.com/models) \u2014 no Router support yet)`;
@@ -192245,6 +192458,15 @@ function formatModelLabel(params) {
192245
192458
  }
192246
192459
  return base;
192247
192460
  }
192461
+ function buildTrialDisclosure(owner) {
192462
+ const consoleUrl = owner ? `https://pullfrog.com/console/${owner}` : "https://pullfrog.com/console";
192463
+ return [
192464
+ "---",
192465
+ "",
192466
+ "> [!IMPORTANT]",
192467
+ `> **This run used the free trial model.** \`DeepSeek Flash\` is fast and cheap \u2014 expect lighter work than a frontier model. [Connect a subscription or API key \u2192](${consoleUrl})`
192468
+ ].join("\n");
192469
+ }
192248
192470
  function buildPullfrogFooter(params) {
192249
192471
  const parts = [];
192250
192472
  if (params.shaPinned) {
@@ -192277,10 +192499,13 @@ function buildPullfrogFooter(params) {
192277
192499
  );
192278
192500
  }
192279
192501
  const allParts = [...parts, "[\u{1D54F}](https://x.com/pullfrogai)"];
192502
+ const disclosure = params.clamped?.reason === "trial" ? `${buildTrialDisclosure(params.owner)}
192503
+
192504
+ ` : "";
192280
192505
  return `
192281
192506
 
192282
192507
  ${PULLFROG_DIVIDER}
192283
- <sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
192508
+ ${disclosure}<sup>${FROG_LOGO}&nbsp;&nbsp;\uFF5C ${allParts.join(" \uFF5C ")}</sup>`;
192284
192509
  }
192285
192510
  function stripExistingFooter(body) {
192286
192511
  const dividerIndex = body.indexOf(PULLFROG_DIVIDER);
@@ -192434,7 +192659,8 @@ function buildCommentFooter(ctx, customParts) {
192434
192659
  clamped: ctx.toolState.modelClamped,
192435
192660
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
192436
192661
  shaPinned: ctx.toolState.shaPinned,
192437
- oss: ctx.oss
192662
+ oss: ctx.oss,
192663
+ owner: ctx.repo.owner
192438
192664
  });
192439
192665
  }
192440
192666
  function buildImplementPlanLink(ctx, issueNumber, commentId) {
@@ -193433,7 +193659,8 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
193433
193659
  clamped: ctx.toolState.modelClamped,
193434
193660
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
193435
193661
  shaPinned: ctx.toolState.shaPinned,
193436
- oss: ctx.oss
193662
+ oss: ctx.oss,
193663
+ owner: ctx.repo.owner
193437
193664
  });
193438
193665
  return await ctx.octokit.rest.pulls.submitReview({
193439
193666
  owner: params.owner,
@@ -196194,7 +196421,8 @@ function buildPrBodyWithFooter(ctx, body) {
196194
196421
  clamped: ctx.toolState.modelClamped,
196195
196422
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
196196
196423
  shaPinned: ctx.toolState.shaPinned,
196197
- oss: ctx.oss
196424
+ oss: ctx.oss,
196425
+ owner: ctx.repo.owner
196198
196426
  });
196199
196427
  const bodyWithoutFooter = stripExistingFooter(fixDoubleEscapedString(body));
196200
196428
  return `${bodyWithoutFooter}${footer}`;
@@ -197501,10 +197729,12 @@ function isByokSetupError(text) {
197501
197729
  var SECRETS_UNAVAILABLE_MARKER = "couldn't load your Pullfrog secrets";
197502
197730
  var ROUTER_UNFUNDED_MARKER = "your Pullfrog Router balance is empty";
197503
197731
  var CREDENTIAL_REJECTED_MARKER = "was rejected by its provider";
197504
- function buildKeyError(params) {
197505
- if (params.secretsUnavailable) return buildSecretsUnavailableError(params);
197506
- if (params.routerUnfunded) return buildRouterUnfundedError(params);
197507
- return buildMissingApiKeyError(params);
197732
+ var NoUsableCredentialError = class extends Error {
197733
+ };
197734
+ function throwKeyError(params) {
197735
+ if (params.secretsUnavailable) throw new Error(buildSecretsUnavailableError(params));
197736
+ if (params.routerUnfunded) throw new NoUsableCredentialError(buildRouterUnfundedError(params));
197737
+ throw new NoUsableCredentialError(buildMissingApiKeyError(params));
197508
197738
  }
197509
197739
  function buildRouterUnfundedError(params) {
197510
197740
  const billingUrl = `${getApiUrl()}/console/${params.owner}#billing`;
@@ -197703,61 +197933,55 @@ function validateAgentApiKey(params) {
197703
197933
  validateVertexSetup({ owner: params.owner, name: params.name });
197704
197934
  return;
197705
197935
  }
197706
- validateBedrockSetup({ owner: params.owner, name: params.name });
197707
- return;
197936
+ if (process.env[BEDROCK_MODEL_ID_ENV]?.trim() === params.model) {
197937
+ validateBedrockSetup({ owner: params.owner, name: params.name });
197938
+ return;
197939
+ }
197708
197940
  }
197709
197941
  if (params.agent.name === "opencode") {
197710
197942
  if (params.authorized.has(params.model)) return;
197711
197943
  const reason = getModelsFailure();
197712
197944
  if (reason) throw new Error(reason);
197713
197945
  if (getModelEnvVars(params.model).some(hasEnvVar2)) return;
197714
- throw new Error(
197715
- buildKeyError({
197716
- owner: params.owner,
197717
- name: params.name,
197718
- model: params.model,
197719
- secretsUnavailable: params.secretsUnavailable,
197720
- routerUnfunded: params.routerUnfunded
197721
- })
197722
- );
197723
- }
197724
- if (hasSingleProviderAuth(params.agent.name)) return;
197725
- throw new Error(
197726
- buildKeyError({
197946
+ throwKeyError({
197727
197947
  owner: params.owner,
197728
197948
  name: params.name,
197729
197949
  model: params.model,
197730
197950
  secretsUnavailable: params.secretsUnavailable,
197731
197951
  routerUnfunded: params.routerUnfunded
197732
- })
197733
- );
197952
+ });
197953
+ }
197954
+ if (hasSingleProviderAuth(params.agent.name)) return;
197955
+ throwKeyError({
197956
+ owner: params.owner,
197957
+ name: params.name,
197958
+ model: params.model,
197959
+ secretsUnavailable: params.secretsUnavailable,
197960
+ routerUnfunded: params.routerUnfunded
197961
+ });
197734
197962
  }
197735
197963
  if (params.agent.name === "opencode") {
197736
197964
  if ([...params.authorized].some(modelHasRuntimeAuth)) return;
197737
197965
  const reason = getModelsFailure();
197738
197966
  if (reason) throw new Error(reason);
197739
- throw new Error(
197740
- buildKeyError({
197741
- owner: params.owner,
197742
- name: params.name,
197743
- secretsUnavailable: params.secretsUnavailable,
197744
- routerUnfunded: params.routerUnfunded
197745
- })
197746
- );
197747
- }
197748
- if (hasSingleProviderAuth(params.agent.name)) return;
197749
- throw new Error(
197750
- buildKeyError({
197967
+ throwKeyError({
197751
197968
  owner: params.owner,
197752
197969
  name: params.name,
197753
197970
  secretsUnavailable: params.secretsUnavailable,
197754
197971
  routerUnfunded: params.routerUnfunded
197755
- })
197756
- );
197972
+ });
197973
+ }
197974
+ if (hasSingleProviderAuth(params.agent.name)) return;
197975
+ throwKeyError({
197976
+ owner: params.owner,
197977
+ name: params.name,
197978
+ secretsUnavailable: params.secretsUnavailable,
197979
+ routerUnfunded: params.routerUnfunded
197980
+ });
197757
197981
  }
197758
197982
  function isApiKeyAuthError(text) {
197759
197983
  if (!text) return false;
197760
- return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /Incorrect API key provided/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text) || /Failed to authenticate\. API Error:/i.test(text) || /Your api key:.*is invalid/i.test(text) || isMalformedKeyError(text) || isClaudeSubscriptionDisabledError(text) || isClaudeSessionLimitError(text) || isOAuthCredentialExpiredError(text);
197984
+ return text.includes(MISSING_KEY_MARKER) || /Invalid API key/i.test(text) || /Incorrect API key provided/i.test(text) || /\bUser not found\b/i.test(text) || /\bInvalid authentication\b/i.test(text) || /authentication_error/i.test(text) || /Invalid bearer token/i.test(text) || /api_error_status\s*=\s*401/i.test(text) || /API Error:\s*401/i.test(text) || /Failed to authenticate\. API Error:/i.test(text) || /Your api key:.*is invalid/i.test(text) || isMalformedKeyError(text) || isClaudeSubscriptionDisabledError(text) || isKimiPlanTierError(text) || isClaudeSessionLimitError(text) || isOAuthCredentialExpiredError(text);
197761
197985
  }
197762
197986
  function isOAuthCredentialExpiredError(text) {
197763
197987
  return (
@@ -197778,6 +198002,9 @@ function isMalformedKeyError(text) {
197778
198002
  function isClaudeSubscriptionDisabledError(text) {
197779
198003
  return /disabled Claude subscription access/i.test(text);
197780
198004
  }
198005
+ function isKimiPlanTierError(text) {
198006
+ return /Your current (?:subscription does not have access to|plan supports only)\b/i.test(text);
198007
+ }
197781
198008
  var CLAUDE_SESSION_LIMIT_PATTERN = /hit your (?:session|weekly|five-hour|Opus|Sonnet|Haiku)\s+limit(?:\s*·\s*resets\s+([^"\n]+))?/i;
197782
198009
  function isClaudeSessionLimitError(text) {
197783
198010
  return CLAUDE_SESSION_LIMIT_PATTERN.test(text);
@@ -197851,6 +198078,13 @@ function formatApiKeyErrorSummary(params) {
197851
198078
  `[Add repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
197852
198079
  ].join("\n");
197853
198080
  }
198081
+ if (isKimiPlanTierError(params.raw)) {
198082
+ return [
198083
+ "**Your Kimi membership doesn't include the model this run asked for.** Kimi K3 needs a Moderato plan or above, and K3's 1M context and Kimi K2 HighSpeed need Allegretto or above. Upgrade the membership, or switch this repo to **Kimi K2**, which every tier includes.",
198084
+ "",
198085
+ `[Kimi plans \u2192](https://www.kimi.com/membership/pricing) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/kimi-code) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198086
+ ].join("\n");
198087
+ }
197854
198088
  if (isOAuthCredentialExpiredError(params.raw)) {
197855
198089
  return [
197856
198090
  `**Your provider OAuth credential has expired or been revoked.** Re-authenticate the provider connection (e.g. \`pullfrog auth claude\` / \`pullfrog auth codex\` / \`pullfrog auth grok\`), then re-trigger the run.`,
@@ -197868,9 +198102,9 @@ function formatApiKeyErrorSummary(params) {
197868
198102
  ].join("\n");
197869
198103
  }
197870
198104
  return [
197871
- `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.`,
198105
+ `**Your LLM provider API key was rejected.** Rotate the key in your provider dashboard, then update the copy Pullfrog uses \u2014 wherever you saved it.`,
197872
198106
  "",
197873
- `[Update repo secret \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
198107
+ `[Pullfrog secrets \u2192](${getApiUrl()}/console/${params.owner}) \xB7 [Repo secrets \u2192](${githubSecretsUrl}) \xB7 [Model settings \u2192](${settingsUrl}) \xB7 [Setup docs \u2192](https://docs.pullfrog.com/keys) \xB7 [Ask in Discord \u2192](https://discord.gg/8y96raFg8e)`
197874
198108
  ].join("\n");
197875
198109
  }
197876
198110
 
@@ -199096,6 +199330,9 @@ var DebugInput = type.enumerated("disabled", "enabled");
199096
199330
  var JsonPayload = type({
199097
199331
  "~pullfrog": "true",
199098
199332
  version: "string",
199333
+ // the server's run type, forwarded verbatim to run-context. optional so a
199334
+ // payload from an older server build still parses against a newer action.
199335
+ "type?": "string | undefined",
199099
199336
  "model?": "string | undefined",
199100
199337
  "modelExplicit?": "boolean | undefined",
199101
199338
  "effort?": "number | string | undefined",
@@ -199581,7 +199818,8 @@ ${ctx.error}` : ctx.error;
199581
199818
  clamped: ctx.toolState.modelClamped,
199582
199819
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
199583
199820
  shaPinned: ctx.toolState.shaPinned,
199584
- oss: ctx.toolState.oss
199821
+ oss: ctx.toolState.oss,
199822
+ owner: repoContext.owner
199585
199823
  });
199586
199824
  const body = `${formattedError}${footer}`;
199587
199825
  const comment = ctx.toolState.progressComment;
@@ -199664,7 +199902,7 @@ async function mintProxyKey(ctx) {
199664
199902
  }
199665
199903
  }
199666
199904
  async function buildProxyTokenHeaders(ctx) {
199667
- const fundingSource = ctx.oss ? "oss" : "router";
199905
+ const fundingSource = ctx.fundingSource;
199668
199906
  if (ctx.oidcCredentials) {
199669
199907
  const creds = ctx.oidcCredentials;
199670
199908
  const oidcToken = await op(() => fetchIdTokenFromStash(creds), {
@@ -199696,7 +199934,7 @@ async function resolveProxyModel(ctx) {
199696
199934
  const key = await mintProxyKey({
199697
199935
  oidcCredentials: ctx.oidcCredentials,
199698
199936
  repo: ctx.repo,
199699
- oss: ctx.oss
199937
+ fundingSource: ctx.oss ? "oss" : "router"
199700
199938
  });
199701
199939
  if (!key) return;
199702
199940
  process.env.OPENROUTER_API_KEY = key;
@@ -199768,6 +200006,26 @@ async function runProxyResolution(ctx) {
199768
200006
  throw error62;
199769
200007
  }
199770
200008
  }
200009
+ async function resolveTrialFallback(ctx) {
200010
+ if (process.env.PULLFROG_MODEL?.trim()) return false;
200011
+ if (!ctx.oidcCredentials && !isLocalApiUrl()) return false;
200012
+ const key = await mintProxyKey({
200013
+ oidcCredentials: ctx.oidcCredentials,
200014
+ repo: ctx.repo,
200015
+ fundingSource: "trial"
200016
+ });
200017
+ if (!key) return false;
200018
+ process.env.OPENROUTER_API_KEY = key;
200019
+ core9.setSecret(key);
200020
+ ctx.payload.proxyModel = DEFAULT_PROXY_MODEL;
200021
+ ctx.toolState.model = DEFAULT_PROXY_MODEL;
200022
+ ctx.toolState.modelClamped = {
200023
+ from: ctx.configuredModel ?? DEFAULT_PROXY_MODEL,
200024
+ reason: "trial"
200025
+ };
200026
+ log.info(`\xBB proxy: trial \u2192 ${DEFAULT_PROXY_MODEL}`);
200027
+ return true;
200028
+ }
199771
200029
 
199772
200030
  // utils/prSummary.ts
199773
200031
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
@@ -199945,8 +200203,9 @@ async function fetchRunContext(params) {
199945
200203
  if (params.oidcToken) {
199946
200204
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
199947
200205
  }
200206
+ const query = params.runType ? `?type=${encodeURIComponent(params.runType)}` : "";
199948
200207
  const response = await apiFetch({
199949
- path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
200208
+ path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context${query}`,
199950
200209
  headers,
199951
200210
  signal: controller.signal
199952
200211
  });
@@ -199983,7 +200242,8 @@ async function fetchRunContext(params) {
199983
200242
  proxyModel: data.proxyModel,
199984
200243
  dbSecrets: data.dbSecrets,
199985
200244
  secretsUnavailable: data.secretsUnavailable,
199986
- routerUnfunded: data.routerUnfunded
200245
+ routerUnfunded: data.routerUnfunded,
200246
+ trialFallback: data.trialFallback
199987
200247
  };
199988
200248
  } catch {
199989
200249
  clearTimeout(timeoutId);
@@ -200024,7 +200284,7 @@ async function resolveRunContextData(params) {
200024
200284
  retries: [100, 500],
200025
200285
  bail: (error62) => !isTransientOctokitError(error62)
200026
200286
  })(),
200027
- fetchRunContext({ token: params.token, repoContext, oidcToken })
200287
+ fetchRunContext({ token: params.token, repoContext, oidcToken, runType: params.runType })
200028
200288
  ]);
200029
200289
  return {
200030
200290
  repo: {
@@ -200042,7 +200302,8 @@ async function resolveRunContextData(params) {
200042
200302
  // a failed mint on a runner that should have been able to mint is the same
200043
200303
  // outcome as the server-side failure: the run never sees stored secrets.
200044
200304
  secretsUnavailable: runContext.secretsUnavailable || !!process.env.ACTIONS_ID_TOKEN_REQUEST_URL && oidcToken === void 0,
200045
- routerUnfunded: runContext.routerUnfunded
200305
+ routerUnfunded: runContext.routerUnfunded,
200306
+ trialFallback: runContext.trialFallback
200046
200307
  };
200047
200308
  }
200048
200309
 
@@ -201122,7 +201383,11 @@ async function main() {
201122
201383
  resolveGit();
201123
201384
  const jobToken = getJobToken();
201124
201385
  const initialOctokit = createOctokit(jobToken);
201125
- const runContext = await resolveRunContextData({ octokit: initialOctokit, token: jobToken });
201386
+ const runContext = await resolveRunContextData({
201387
+ octokit: initialOctokit,
201388
+ token: jobToken,
201389
+ runType: typeof resolvedPromptInput === "string" ? void 0 : resolvedPromptInput.type
201390
+ });
201126
201391
  timer.checkpoint("runContextData");
201127
201392
  const payload = resolvePayload(resolvedPromptInput, runContext.repoSettings);
201128
201393
  const toolState = initToolState({
@@ -201303,7 +201568,7 @@ async function main() {
201303
201568
  }
201304
201569
  const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
201305
201570
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
201306
- const agent2 = resolveAgent({
201571
+ let agent2 = resolveAgent({
201307
201572
  model: resolvedModel,
201308
201573
  proxyModel: payload.proxyModel,
201309
201574
  // the account opt-in and the canary arm are both admissions to codex, so
@@ -201315,15 +201580,32 @@ async function main() {
201315
201580
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
201316
201581
  toolState.model = effectiveModel;
201317
201582
  if (!payload.proxyModel) {
201318
- validateAgentApiKey({
201319
- agent: agent2,
201320
- model: effectiveModel,
201321
- authorized: getAuthorizedModels(),
201322
- owner: runContext.repo.owner,
201323
- name: runContext.repo.name,
201324
- secretsUnavailable: runContext.secretsUnavailable,
201325
- routerUnfunded: runContext.routerUnfunded
201326
- });
201583
+ try {
201584
+ validateAgentApiKey({
201585
+ agent: agent2,
201586
+ model: effectiveModel,
201587
+ authorized: getAuthorizedModels(),
201588
+ owner: runContext.repo.owner,
201589
+ name: runContext.repo.name,
201590
+ secretsUnavailable: runContext.secretsUnavailable,
201591
+ routerUnfunded: runContext.routerUnfunded
201592
+ });
201593
+ } catch (missingKey) {
201594
+ const funded = runContext.trialFallback && missingKey instanceof NoUsableCredentialError ? await resolveTrialFallback({
201595
+ payload,
201596
+ configuredModel: effectiveModel,
201597
+ oidcCredentials,
201598
+ repo: runContext.repo,
201599
+ toolState
201600
+ }) : false;
201601
+ if (!funded) throw missingKey;
201602
+ agent2 = resolveAgent({
201603
+ model: resolvedModel,
201604
+ proxyModel: payload.proxyModel,
201605
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
201606
+ });
201607
+ toolState.agent = agent2.name;
201608
+ }
201327
201609
  }
201328
201610
  await setupGit({
201329
201611
  gitToken: tokenRef.gitToken,