pullfrog 0.1.90 → 0.1.91

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
@@ -83904,7 +83904,7 @@ var require_Element = __commonJS({
83904
83904
  var FilteredElementList = require_FilteredElementList();
83905
83905
  var DOMException2 = require_DOMException();
83906
83906
  var DOMTokenList = require_DOMTokenList();
83907
- var select = require_select();
83907
+ var select2 = require_select();
83908
83908
  var ContainerNode = require_ContainerNode();
83909
83909
  var ChildNode = require_ChildNode();
83910
83910
  var NonDocumentTypeChildNode = require_NonDocumentTypeChildNode();
@@ -84665,7 +84665,7 @@ var require_Element = __commonJS({
84665
84665
  this.className = v;
84666
84666
  } },
84667
84667
  matches: { value: function(selector) {
84668
- return select.matches(this, selector);
84668
+ return select2.matches(this, selector);
84669
84669
  } },
84670
84670
  closest: { value: function(selector) {
84671
84671
  var el = this;
@@ -84678,10 +84678,10 @@ var require_Element = __commonJS({
84678
84678
  return null;
84679
84679
  } },
84680
84680
  querySelector: { value: function(selector) {
84681
- return select(selector, this)[0];
84681
+ return select2(selector, this)[0];
84682
84682
  } },
84683
84683
  querySelectorAll: { value: function(selector) {
84684
- var nodes = select(selector, this);
84684
+ var nodes = select2(selector, this);
84685
84685
  return nodes.item ? nodes : new NodeList(nodes);
84686
84686
  } }
84687
84687
  });
@@ -85209,7 +85209,7 @@ var require_DocumentFragment = __commonJS({
85209
85209
  var NodeList = require_NodeList();
85210
85210
  var ContainerNode = require_ContainerNode();
85211
85211
  var Element = require_Element();
85212
- var select = require_select();
85212
+ var select2 = require_select();
85213
85213
  var utils = require_utils8();
85214
85214
  function DocumentFragment(doc) {
85215
85215
  ContainerNode.call(this);
@@ -85238,7 +85238,7 @@ var require_DocumentFragment = __commonJS({
85238
85238
  context.isHTML = true;
85239
85239
  context.getElementsByTagName = Element.prototype.getElementsByTagName;
85240
85240
  context.nextElement = Object.getOwnPropertyDescriptor(Element.prototype, "firstElementChild").get;
85241
- var nodes = select(selector, context);
85241
+ var nodes = select2(selector, context);
85242
85242
  return nodes.item ? nodes : new NodeList(nodes);
85243
85243
  } },
85244
85244
  // Utility methods
@@ -88353,7 +88353,7 @@ var require_Document = __commonJS({
88353
88353
  var NodeIterator = require_NodeIterator();
88354
88354
  var NodeFilter = require_NodeFilter();
88355
88355
  var URL2 = require_URL();
88356
- var select = require_select();
88356
+ var select2 = require_select();
88357
88357
  var events = require_events3();
88358
88358
  var xml = require_xmlnames();
88359
88359
  var html = require_htmlelts();
@@ -88914,10 +88914,10 @@ var require_Document = __commonJS({
88914
88914
  return this._templateDocCache;
88915
88915
  } },
88916
88916
  querySelector: { value: function(selector) {
88917
- return select(selector, this)[0];
88917
+ return select2(selector, this)[0];
88918
88918
  } },
88919
88919
  querySelectorAll: { value: function(selector) {
88920
- var nodes = select(selector, this);
88920
+ var nodes = select2(selector, this);
88921
88921
  return nodes.item ? nodes : new NodeList(nodes);
88922
88922
  } }
88923
88923
  });
@@ -105082,10 +105082,12 @@ async function preflightClaudeSubscription(params) {
105082
105082
  if (res.status !== 401 && res.status !== 403 && res.status !== 429)
105083
105083
  return { usable: true, status: res.status };
105084
105084
  const body = await res.text().catch(() => "");
105085
+ const reset = Number(res.headers.get("anthropic-ratelimit-unified-reset"));
105085
105086
  return {
105086
105087
  usable: false,
105087
105088
  status: res.status,
105088
- reason: `${res.status}: ${extractApiErrorMessage(body)}`
105089
+ reason: `${res.status}: ${extractApiErrorMessage(body)}`,
105090
+ resetAt: res.status === 429 && reset > 0 ? new Date(reset * 1e3) : void 0
105089
105091
  };
105090
105092
  }
105091
105093
  function extractApiErrorMessage(body) {
@@ -105729,7 +105731,7 @@ var import_semver = __toESM(require_semver2(), 1);
105729
105731
  // package.json
105730
105732
  var package_default = {
105731
105733
  name: "pullfrog",
105732
- version: "0.1.90",
105734
+ version: "0.1.91",
105733
105735
  cliContractVersion: "1",
105734
105736
  type: "module",
105735
105737
  bin: {
@@ -108001,6 +108003,17 @@ function parseXaiAuthBody(raw2) {
108001
108003
  var CODEX_AUTH_ENV = "CODEX_AUTH_JSON";
108002
108004
  var XAI_AUTH_ENV = "GROK_AUTH_JSON";
108003
108005
  var installedSubscriptionPaths = /* @__PURE__ */ new Map();
108006
+ function clearInstalledSubscription(name) {
108007
+ const path4 = installedSubscriptionPaths.get(name);
108008
+ if (!path4 || !existsSync4(path4)) return;
108009
+ const value2 = JSON.parse(readFileSync2(path4, "utf8"));
108010
+ if (!value2 || typeof value2 !== "object") return;
108011
+ if (name === "CODEX_AUTH_JSON" && "openai" in value2) delete value2.openai;
108012
+ if (name === "GROK_AUTH_JSON" && "xai" in value2) delete value2.xai;
108013
+ writeFileSync4(path4, `${JSON.stringify(value2)}
108014
+ `, { mode: 384 });
108015
+ installedSubscriptionPaths.delete(name);
108016
+ }
108004
108017
  var PULLFROG_DATA_DIR = "/var/lib/pullfrog";
108005
108018
  function isRejectedChain(body) {
108006
108019
  if (!body.refresh_rejected_at) return false;
@@ -116693,6 +116706,9 @@ function captureAuthorizedModels(cliPath) {
116693
116706
  }
116694
116707
  log.debug(`\xBB opencode authorized: ${authorized.size} models`);
116695
116708
  }
116709
+ function authorizeModel(model) {
116710
+ getAuthorizedModels().add(model);
116711
+ }
116696
116712
  function getAuthorizedModels() {
116697
116713
  if (!authorized) {
116698
116714
  throw new Error("getAuthorizedModels called before captureAuthorizedModels");
@@ -130812,8 +130828,8 @@ function liftWireOnlyMaterial(message, kind) {
130812
130828
  };
130813
130829
  }
130814
130830
  function codecResultValidator(codec2, method) {
130815
- const probe = codec2.validateResult(method, void 0);
130816
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
130831
+ const probe2 = codec2.validateResult(method, void 0);
130832
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
130817
130833
  return { "~standard": {
130818
130834
  version: 1,
130819
130835
  vendor: "mcp-wire-codec",
@@ -145189,8 +145205,8 @@ function liftWireOnlyMaterial2(message, kind) {
145189
145205
  };
145190
145206
  }
145191
145207
  function codecResultValidator2(codec2, method) {
145192
- const probe = codec2.validateResult(method, void 0);
145193
- if (!probe.ok && probe.reason === "not-in-era") return void 0;
145208
+ const probe2 = codec2.validateResult(method, void 0);
145209
+ if (!probe2.ok && probe2.reason === "not-in-era") return void 0;
145194
145210
  return { "~standard": {
145195
145211
  version: 1,
145196
145212
  vendor: "mcp-wire-codec",
@@ -188369,10 +188385,10 @@ function CommitChangesTool(ctx) {
188369
188385
  }
188370
188386
  const requested = new Set(params.files);
188371
188387
  const known = new Set(changes.map((c) => c.path));
188372
- const unknown4 = [...requested].filter((p) => !known.has(p));
188373
- if (unknown4.length > 0) {
188388
+ const unknown5 = [...requested].filter((p) => !known.has(p));
188389
+ if (unknown5.length > 0) {
188374
188390
  throw new Error(
188375
- `no detected change at: ${unknown4.join(", ")} \u2014 run git status to list changed paths.`
188391
+ `no detected change at: ${unknown5.join(", ")} \u2014 run git status to list changed paths.`
188376
188392
  );
188377
188393
  }
188378
188394
  changes = changes.filter((c) => requested.has(c.path));
@@ -188765,15 +188781,6 @@ function PushTagsTool(ctx) {
188765
188781
  // utils/buildPullfrogFooter.ts
188766
188782
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
188767
188783
  var FROG_LOGO = `<a href="https://pullfrog.com"><picture><source media="(prefers-color-scheme: dark)" srcset="https://pullfrog.com/logos/frog-white-full-18px.png"><img src="https://pullfrog.com/logos/frog-green-full-18px.png" width="9px" height="9px" style="vertical-align: middle; " alt="Pullfrog"></picture></a>`;
188768
- function providerDisplayName(slug2) {
188769
- try {
188770
- const key = getModelProvider(slug2);
188771
- const meta3 = providers[key];
188772
- return meta3?.displayName ?? key;
188773
- } catch {
188774
- return slug2;
188775
- }
188776
- }
188777
188784
  function formatModelLabel(params) {
188778
188785
  const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
188779
188786
  // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
@@ -188788,9 +188795,6 @@ function formatModelLabel(params) {
188788
188795
  return `${ossBase} (${configured} not used \u2014 pick one of the [funded models](https://docs.pullfrog.com/models#pullfrog-for-oss) or add a [provider key](https://docs.pullfrog.com/keys) to run your own)`;
188789
188796
  }
188790
188797
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
188791
- if (params.fallbackFrom) {
188792
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
188793
- }
188794
188798
  if (params.clamped?.reason === "trial") {
188795
188799
  return `${base} (model usage covered by Pullfrog)`;
188796
188800
  }
@@ -188831,7 +188835,6 @@ function buildPullfrogFooter(params) {
188831
188835
  parts.push(
188832
188836
  `Using ${formatModelLabel({
188833
188837
  model: params.model,
188834
- fallbackFrom: params.fallbackFrom,
188835
188838
  clamped: params.clamped,
188836
188839
  unselectedProxyDefault: params.unselectedProxyDefault,
188837
188840
  oss: params.oss
@@ -189031,7 +189034,6 @@ function buildCommentFooter(ctx, customParts) {
189031
189034
  } : void 0,
189032
189035
  customParts,
189033
189036
  model: ctx.toolState.model,
189034
- fallbackFrom: ctx.toolState.modelFallback?.from,
189035
189037
  clamped: ctx.toolState.modelClamped,
189036
189038
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
189037
189039
  oss: ctx.oss,
@@ -190021,7 +190023,6 @@ async function createAndSubmitWithFooter(ctx, params, opts) {
190021
190023
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
190022
190024
  customParts,
190023
190025
  model: ctx.toolState.model,
190024
- fallbackFrom: ctx.toolState.modelFallback?.from,
190025
190026
  clamped: ctx.toolState.modelClamped,
190026
190027
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
190027
190028
  oss: ctx.oss,
@@ -192397,7 +192398,6 @@ function buildPrBodyWithFooter(ctx, body) {
192397
192398
  triggeredBy: true,
192398
192399
  workflowRun: ctx.runId ? { owner: ctx.repo.owner, repo: ctx.repo.name, runId: ctx.runId, jobId: ctx.jobId } : void 0,
192399
192400
  model: ctx.toolState.model,
192400
- fallbackFrom: ctx.toolState.modelFallback?.from,
192401
192401
  clamped: ctx.toolState.modelClamped,
192402
192402
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
192403
192403
  oss: ctx.oss,
@@ -194546,17 +194546,17 @@ async function verifyCredential(params) {
194546
194546
  const value2 = params.value.trim();
194547
194547
  if (value2.length === 0) return "dead";
194548
194548
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") return verifyClaudeSubscription(value2);
194549
- const probe = PROBES[params.envVar];
194550
- if (!probe) return "unknown";
194551
- const request2 = probe.request(value2);
194549
+ const probe2 = PROBES[params.envVar];
194550
+ if (!probe2) return "unknown";
194551
+ const request2 = probe2.request(value2);
194552
194552
  try {
194553
194553
  const response = await fetch(request2.url, {
194554
194554
  headers: request2.headers,
194555
194555
  signal: AbortSignal.timeout(5e3)
194556
194556
  });
194557
194557
  if (response.ok) return "alive";
194558
- if (probe.deadWhen) {
194559
- return probe.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
194558
+ if (probe2.deadWhen) {
194559
+ return probe2.deadWhen(response.status, await response.text()) ? "dead" : "unknown";
194560
194560
  }
194561
194561
  return response.status === 401 ? "dead" : "unknown";
194562
194562
  } catch {
@@ -194587,14 +194587,6 @@ async function checkOne(params) {
194587
194587
  const verdict = await verifyCredential({ envVar: params.envVar, value: value2 });
194588
194588
  return verdict === "dead" ? { severity: "dead", reason: void 0 } : null;
194589
194589
  }
194590
- function pickReplacement(params) {
194591
- const candidates = modelAliases.filter(
194592
- (alias) => !alias.hidden && !alias.fallback && params.authorized.has(alias.resolve) && // a model with no known env vars is routable through credentials we can't
194593
- // see (Codex auth lands as an on-disk auth.json), so it stays a candidate.
194594
- !envVarsFor(alias.resolve).some((envVar) => params.deadVars.has(envVar))
194595
- );
194596
- return (candidates.find((alias) => alias.preferred) ?? candidates[0])?.resolve;
194597
- }
194598
194590
  async function checkConfiguredCredentials(params) {
194599
194591
  if (!params.model) return { kind: "ok" };
194600
194592
  const present = envVarsFor(params.model).filter(hasEnvVar4);
@@ -194620,12 +194612,396 @@ async function checkConfiguredCredentials(params) {
194620
194612
  failed.forEach(drop);
194621
194613
  const first = failed[0];
194622
194614
  if (!first) return { kind: "ok" };
194623
- const outcome = { credential: first.envVar, reason: first.failure.reason };
194624
- const replacement = pickReplacement({
194625
- authorized: params.authorized,
194626
- deadVars: new Set(failed.map((entry) => entry.envVar))
194615
+ return { kind: "dead", credential: first.envVar, reason: first.failure.reason };
194616
+ }
194617
+
194618
+ // utils/normalizeEnv.ts
194619
+ function hasControlCharacter(value2) {
194620
+ return [...value2].some((ch) => {
194621
+ const code = ch.charCodeAt(0);
194622
+ return code < 32 || code === 127;
194627
194623
  });
194628
- return replacement ? { kind: "fellBack", ...outcome, replacement } : { kind: "dead", ...outcome };
194624
+ }
194625
+ function sanitizeSecret(key, value2) {
194626
+ const trimmed = value2.trim();
194627
+ if (trimmed.length === 0) {
194628
+ log.warning(
194629
+ `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
194630
+ );
194631
+ return null;
194632
+ }
194633
+ if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
194634
+ log.warning(
194635
+ `\xBB ${key} contains a line break or control character and cannot be sent as an HTTP header \u2014 leaving env var unchanged. re-save the secret as a single line.`
194636
+ );
194637
+ return null;
194638
+ }
194639
+ if (trimmed !== value2) {
194640
+ log.warning(
194641
+ `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
194642
+ );
194643
+ }
194644
+ maskSecret(trimmed);
194645
+ return trimmed;
194646
+ }
194647
+ function normalizeEnv() {
194648
+ const upperKeys = /* @__PURE__ */ new Map();
194649
+ for (const key of Object.keys(process.env)) {
194650
+ const upper2 = key.toUpperCase();
194651
+ const existing = upperKeys.get(upper2) || [];
194652
+ existing.push(key);
194653
+ upperKeys.set(upper2, existing);
194654
+ }
194655
+ for (const [upperKey, keys] of upperKeys) {
194656
+ if (keys.length === 1) {
194657
+ const key = keys[0];
194658
+ if (key !== upperKey) {
194659
+ process.env[upperKey] = process.env[key];
194660
+ delete process.env[key];
194661
+ }
194662
+ continue;
194663
+ }
194664
+ const values = keys.map((k2) => process.env[k2]);
194665
+ const uniqueValues = new Set(values);
194666
+ if (uniqueValues.size > 1) {
194667
+ log.warning(
194668
+ `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
194669
+ );
194670
+ }
194671
+ const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
194672
+ const preferredValue = process.env[preferredKey];
194673
+ for (const key of keys) {
194674
+ delete process.env[key];
194675
+ }
194676
+ process.env[upperKey] = preferredValue;
194677
+ }
194678
+ for (const key of Object.keys(process.env)) {
194679
+ if (!isSensitiveEnvName(key)) continue;
194680
+ const value2 = process.env[key];
194681
+ if (typeof value2 !== "string" || value2.length === 0) continue;
194682
+ const sanitized = sanitizeSecret(key, value2);
194683
+ if (sanitized !== null) process.env[key] = sanitized;
194684
+ }
194685
+ }
194686
+
194687
+ // utils/subscriptionCredentials.ts
194688
+ var subscriptionNameSchema = external_exports.enum([
194689
+ "CLAUDE_CODE_OAUTH_TOKEN",
194690
+ "CODEX_AUTH_JSON",
194691
+ "GROK_AUTH_JSON"
194692
+ ]);
194693
+ var subscriptionSchema = external_exports.object({
194694
+ id: external_exports.string(),
194695
+ credentialId: external_exports.string(),
194696
+ name: subscriptionNameSchema,
194697
+ label: external_exports.string(),
194698
+ owner: external_exports.string(),
194699
+ inherited: external_exports.boolean(),
194700
+ shared: external_exports.boolean(),
194701
+ registeredAt: external_exports.string(),
194702
+ /** the provider's last verdict on the current value; null = usable or never checked. */
194703
+ status: external_exports.enum(["rejected", "exhausted"]).nullish(),
194704
+ resetAt: external_exports.string().nullish()
194705
+ });
194706
+ var subscriptionListSchema = external_exports.object({
194707
+ subscriptions: external_exports.array(subscriptionSchema),
194708
+ writable: external_exports.boolean()
194709
+ });
194710
+ var subscriptionInputSchema = external_exports.object({
194711
+ name: subscriptionNameSchema,
194712
+ value: external_exports.string().trim().min(1).refine((value2) => Buffer.byteLength(value2, "utf8") <= 49152),
194713
+ replaceId: external_exports.string().optional()
194714
+ });
194715
+ var credentialCandidateSchema = external_exports.object({
194716
+ id: external_exports.string(),
194717
+ name: external_exports.string(),
194718
+ source: external_exports.enum(["repo", "account", "personal"])
194719
+ });
194720
+ var credentialAccessSchema = external_exports.object({
194721
+ token: external_exports.string(),
194722
+ candidates: external_exports.array(credentialCandidateSchema)
194723
+ });
194724
+ var selectedCredentialSchema = external_exports.object({
194725
+ name: external_exports.string(),
194726
+ value: external_exports.string(),
194727
+ receipt: external_exports.string()
194728
+ });
194729
+
194730
+ // utils/subscriptionProbe.ts
194731
+ var identitySchema = external_exports.object({
194732
+ email: external_exports.string().email().optional(),
194733
+ sub: external_exports.string().optional(),
194734
+ user_id: external_exports.string().optional(),
194735
+ account_id: external_exports.string().optional(),
194736
+ email_verified: external_exports.boolean().optional()
194737
+ });
194738
+ var codexWindowSchema = external_exports.object({ used_percent: external_exports.number().optional(), reset_at: external_exports.number().optional() }).nullish();
194739
+ var codexUsageSchema = external_exports.object({
194740
+ rate_limit: external_exports.object({
194741
+ allowed: external_exports.boolean(),
194742
+ limit_reached: external_exports.boolean(),
194743
+ primary_window: codexWindowSchema,
194744
+ secondary_window: codexWindowSchema
194745
+ }).optional()
194746
+ });
194747
+ var unknown4 = { status: "unknown" };
194748
+ async function readSubscriptionEndpoint(input) {
194749
+ const headers = new Headers({ authorization: `Bearer ${input.token}` });
194750
+ if (input.accountId) headers.set("ChatGPT-Account-Id", input.accountId);
194751
+ if (input.grokBilling) headers.set("x-xai-token-auth", "xai-grok-cli");
194752
+ return fetch(input.url, {
194753
+ headers,
194754
+ redirect: "error",
194755
+ signal: AbortSignal.timeout(1e4)
194756
+ }).catch(() => null);
194757
+ }
194758
+ async function probeSubscription(input) {
194759
+ if (input.name === "CLAUDE_CODE_OAUTH_TOKEN") {
194760
+ const result = await preflightClaudeSubscription({ token: input.value, model: input.model });
194761
+ if (result.usable)
194762
+ return result.status !== void 0 && result.status < 300 ? { status: "usable" } : unknown4;
194763
+ return result.status === 429 ? { status: "exhausted", detail: result.reason, resetAt: result.resetAt } : { status: "rejected", detail: result.reason };
194764
+ }
194765
+ const codex2 = input.name === "CODEX_AUTH_JSON" ? parseCodexAuthBody(input.value) : null;
194766
+ const grok = input.name === "GROK_AUTH_JSON" ? parseXaiAuthBody(input.value) : null;
194767
+ const auth2 = codex2 ?? grok;
194768
+ if (!auth2) return { status: "rejected", detail: "the stored credential is malformed" };
194769
+ if (auth2.refresh_rejected_at)
194770
+ return { status: "rejected", detail: `refresh rejected at ${auth2.refresh_rejected_at}` };
194771
+ if (grok && input.model)
194772
+ return probeInference({
194773
+ name: "XAI_API_KEY",
194774
+ value: grok.tokens.access_token,
194775
+ model: input.model
194776
+ });
194777
+ const response = await readSubscriptionEndpoint({
194778
+ url: codex2 ? "https://chatgpt.com/backend-api/wham/usage" : "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
194779
+ token: auth2.tokens.access_token,
194780
+ accountId: codex2?.tokens.account_id,
194781
+ grokBilling: !!grok
194782
+ });
194783
+ if (response?.status === 401) return { status: "rejected", detail: "rejected with 401" };
194784
+ if (!response?.ok) return unknown4;
194785
+ const body = await response.json().catch(() => null);
194786
+ if (!codex2) return unknown4;
194787
+ const parsed2 = codexUsageSchema.safeParse(body);
194788
+ const limit = parsed2.success ? parsed2.data.rate_limit : void 0;
194789
+ if (!limit) return unknown4;
194790
+ if (limit.allowed && !limit.limit_reached) return { status: "usable" };
194791
+ const windows = [limit.primary_window, limit.secondary_window];
194792
+ const full = windows.filter((window2) => (window2?.used_percent ?? 0) >= 100);
194793
+ const resets = (full.length ? full : windows).flatMap(
194794
+ (window2) => window2?.reset_at ? [window2.reset_at] : []
194795
+ );
194796
+ return {
194797
+ status: "exhausted",
194798
+ detail: "the ChatGPT plan's usage limit is reached",
194799
+ resetAt: resets.length ? new Date(Math.max(...resets) * 1e3) : void 0
194800
+ };
194801
+ }
194802
+ async function probeInference(input) {
194803
+ const anthropic = input.name === "ANTHROPIC_API_KEY";
194804
+ const openai = input.name === "OPENAI_API_KEY";
194805
+ if (!anthropic && !openai && input.name !== "XAI_API_KEY") {
194806
+ const result = await verifyCredential({ envVar: input.name, value: input.value });
194807
+ return result === "dead" ? { status: "rejected", detail: "rejected by its provider" } : result === "alive" ? { status: "usable" } : unknown4;
194808
+ }
194809
+ const headers = new Headers({ "content-type": "application/json" });
194810
+ if (anthropic) {
194811
+ headers.set("x-api-key", input.value);
194812
+ headers.set("anthropic-version", "2023-06-01");
194813
+ } else headers.set("authorization", `Bearer ${input.value}`);
194814
+ const response = await fetch(
194815
+ anthropic ? "https://api.anthropic.com/v1/messages" : openai ? "https://api.openai.com/v1/responses" : "https://api.x.ai/v1/chat/completions",
194816
+ {
194817
+ method: "POST",
194818
+ headers,
194819
+ body: JSON.stringify(
194820
+ openai ? { model: input.model, input: "Reply OK.", max_output_tokens: 16, store: false } : {
194821
+ model: input.model,
194822
+ messages: [{ role: "user", content: "Reply OK." }],
194823
+ max_tokens: 1
194824
+ }
194825
+ ),
194826
+ redirect: "error",
194827
+ signal: AbortSignal.timeout(1e4)
194828
+ }
194829
+ ).catch(() => null);
194830
+ if (response?.status === 400 && input.name === "XAI_API_KEY") {
194831
+ const error49 = external_exports.object({ error: external_exports.string() }).safeParse(await response.json().catch(() => null));
194832
+ return error49.success && error49.data.error.startsWith("Incorrect API key provided.") ? { status: "rejected", detail: "rejected with 400: incorrect API key" } : unknown4;
194833
+ }
194834
+ if (response?.status === 429 && openai) {
194835
+ const error49 = external_exports.object({ error: external_exports.object({ code: external_exports.string() }) }).safeParse(await response.json().catch(() => null));
194836
+ return error49.success && error49.data.error.code === "insufficient_quota" ? { status: "exhausted", detail: "refused with 429: insufficient quota", resetAt: void 0 } : unknown4;
194837
+ }
194838
+ await response?.body?.cancel();
194839
+ if (!response) return unknown4;
194840
+ if (response.ok) return { status: "usable" };
194841
+ if (response.status === 401 || response.status === 403)
194842
+ return { status: "rejected", detail: `rejected with ${response.status}` };
194843
+ if (response.status === 402)
194844
+ return { status: "exhausted", detail: "refused with 402", resetAt: void 0 };
194845
+ return unknown4;
194846
+ }
194847
+
194848
+ // utils/credentialPool.ts
194849
+ var receipts = {};
194850
+ var workflowCredentials = {};
194851
+ function saveReceipts() {
194852
+ for (const receipt of Object.values(receipts)) maskSecret(receipt);
194853
+ saveSecretState("credential_receipts", JSON.stringify(receipts));
194854
+ }
194855
+ var select = op(
194856
+ async (input) => {
194857
+ const response = await apiFetch({
194858
+ path: "/api/runtime/credentials",
194859
+ method: "POST",
194860
+ headers: {
194861
+ authorization: `Bearer ${input.access.token}`,
194862
+ "content-type": "application/json"
194863
+ },
194864
+ body: JSON.stringify({ id: input.candidate.id }),
194865
+ signal: AbortSignal.timeout(35e3)
194866
+ });
194867
+ if (response.status === 404) return null;
194868
+ if (!response.ok)
194869
+ throw new Error("could not load a configured credential; no fallback was charged", {
194870
+ cause: response.status
194871
+ });
194872
+ return selectedCredentialSchema.parse(await response.json());
194873
+ },
194874
+ {
194875
+ retries: [250, 1e3],
194876
+ bail: (error49) => error49 instanceof Error && typeof error49.cause === "number" && error49.cause >= 400 && error49.cause < 500
194877
+ }
194878
+ );
194879
+ function subscriptionForModel(model) {
194880
+ if (!model.includes("/")) return null;
194881
+ const provider2 = getModelProvider(model);
194882
+ return provider2 === "anthropic" ? "CLAUDE_CODE_OAUTH_TOKEN" : provider2 === "openai" ? "CODEX_AUTH_JSON" : provider2 === "xai" ? "GROK_AUTH_JSON" : null;
194883
+ }
194884
+ async function initializeCredentialPool(input) {
194885
+ const access = input.ctx.credentialAccess;
194886
+ if (!access) return;
194887
+ const model = discoveryModel(input.slug);
194888
+ maskSecret(access.token);
194889
+ const names = /* @__PURE__ */ new Set([
194890
+ ...access.candidates.map((candidate) => candidate.name),
194891
+ ...subscriptionNameSchema.options,
194892
+ "ANTHROPIC_API_KEY",
194893
+ "OPENAI_API_KEY",
194894
+ "XAI_API_KEY"
194895
+ ]);
194896
+ for (const name of names) {
194897
+ const value2 = process.env[name];
194898
+ if (value2 && !input.injected.includes(name)) workflowCredentials[name] = value2;
194899
+ }
194900
+ for (const name of subscriptionNameSchema.options) {
194901
+ if (model && subscriptionForModel(model) !== name) continue;
194902
+ if (workflowCredentials[name]) continue;
194903
+ const candidate = access.candidates.find((item) => item.name === name);
194904
+ if (!candidate) continue;
194905
+ const selected = await select({ access, candidate }).catch(() => {
194906
+ log.warning(`\xBB could not load ${name} for model discovery; selection will retry if needed`);
194907
+ return null;
194908
+ });
194909
+ if (!selected) continue;
194910
+ const value2 = sanitizeSecret(name, selected.value);
194911
+ if (!value2) continue;
194912
+ process.env[name] = value2;
194913
+ receipts[name] = selected.receipt;
194914
+ }
194915
+ saveReceipts();
194916
+ }
194917
+ function discoveryModel(slug2) {
194918
+ const value2 = process.env.PULLFROG_MODEL?.trim() || slug2?.trim();
194919
+ if (!value2) return void 0;
194920
+ return resolveCliModel(value2) ?? (value2.includes("/") ? value2 : void 0);
194921
+ }
194922
+ function resolvePoolModel(input) {
194923
+ const agent2 = resolveAgent({
194924
+ model: void 0,
194925
+ proxyModel: void 0,
194926
+ codexAgent: input.codexAgent
194927
+ });
194928
+ if (agent2.name === "opencode") return autoSelectModel();
194929
+ const provider2 = agent2.name === "claude" ? "anthropic" : "openai";
194930
+ return modelAliases.find(
194931
+ (alias) => alias.provider === provider2 && alias.preferred && !alias.hidden && !alias.fallback && !alias.routing
194932
+ )?.resolve;
194933
+ }
194934
+ async function selectConfiguredCredential(input) {
194935
+ const access = input.ctx.credentialAccess;
194936
+ const model = input.model;
194937
+ if (!access || !model?.includes("/") || getProviderGatewayUrl(model)) return false;
194938
+ const subscription = subscriptionForModel(model);
194939
+ const names = getModelEnvVars(model).filter((name) => name !== subscription);
194940
+ if (subscription) names.unshift(subscription);
194941
+ const candidates = access.candidates.filter((candidate) => names.includes(candidate.name));
194942
+ if (!candidates.length) return false;
194943
+ const refused = [];
194944
+ for (const name of names.filter((item) => workflowCredentials[item])) {
194945
+ const value2 = workflowCredentials[name];
194946
+ const verdict = await probe({ name, value: value2, model });
194947
+ if (usable(verdict)) {
194948
+ activate({ names, name, value: value2, receipt: void 0, model });
194949
+ return true;
194950
+ }
194951
+ refused.push(`${name} from the workflow: ${verdict.detail}`);
194952
+ log.info(`\xBB ${name} from the workflow unavailable; trying the next credential`);
194953
+ }
194954
+ for (const candidate of candidates) {
194955
+ const selected = await select({ access, candidate });
194956
+ if (!selected) continue;
194957
+ const verdict = await probe({ ...selected, model });
194958
+ await reportVerdict({ receipt: selected.receipt, verdict });
194959
+ if (usable(verdict)) {
194960
+ activate({ names, ...selected, model });
194961
+ log.info(`\xBB selected ${candidate.name} from ${candidate.source} scope`);
194962
+ return true;
194963
+ }
194964
+ refused.push(`${candidate.name} from ${candidate.source} scope: ${verdict.detail}`);
194965
+ log.info(
194966
+ `\xBB ${candidate.name} from ${candidate.source} scope unavailable; trying the next credential`
194967
+ );
194968
+ }
194969
+ throw new Error(
194970
+ `all configured credentials for ${model} were rejected or exhausted (${refused.join("; ")}); no other model or provider was selected`
194971
+ );
194972
+ }
194973
+ async function probe(input) {
194974
+ const subscription = subscriptionNameSchema.safeParse(input.name);
194975
+ const model = stripProviderPrefix(input.model);
194976
+ return subscription.success ? probeSubscription({ name: subscription.data, value: input.value, model }) : probeInference({ ...input, model });
194977
+ }
194978
+ function usable(verdict) {
194979
+ return verdict.status !== "rejected" && verdict.status !== "exhausted";
194980
+ }
194981
+ async function reportVerdict(input) {
194982
+ if (input.verdict.status === "unknown") return;
194983
+ await apiFetch({
194984
+ path: "/api/runtime/credentials/status",
194985
+ method: "POST",
194986
+ headers: { authorization: `Bearer ${input.receipt}`, "content-type": "application/json" },
194987
+ body: JSON.stringify(input.verdict),
194988
+ signal: AbortSignal.timeout(5e3)
194989
+ }).catch(() => log.debug("\xBB could not record a credential verdict"));
194990
+ }
194991
+ function activate(input) {
194992
+ for (const name of input.names) {
194993
+ delete process.env[name];
194994
+ delete receipts[name];
194995
+ if (name === "CODEX_AUTH_JSON" || name === "GROK_AUTH_JSON") clearInstalledSubscription(name);
194996
+ }
194997
+ const value2 = sanitizeSecret(input.name, input.value);
194998
+ if (!value2) throw new Error("configured credential was empty");
194999
+ process.env[input.name] = value2;
195000
+ if (input.receipt) receipts[input.name] = input.receipt;
195001
+ installCodexAuth();
195002
+ installXaiAuth();
195003
+ saveReceipts();
195004
+ authorizeModel(input.model);
194629
195005
  }
194630
195006
 
194631
195007
  // utils/gitAuthServer.ts
@@ -195331,75 +195707,6 @@ function buildModelAccessError(input) {
195331
195707
  return [headline, "", why, "", cta].join("\n");
195332
195708
  }
195333
195709
 
195334
- // utils/normalizeEnv.ts
195335
- function hasControlCharacter(value2) {
195336
- return [...value2].some((ch) => {
195337
- const code = ch.charCodeAt(0);
195338
- return code < 32 || code === 127;
195339
- });
195340
- }
195341
- function sanitizeSecret(key, value2) {
195342
- const trimmed = value2.trim();
195343
- if (trimmed.length === 0) {
195344
- log.warning(
195345
- `\xBB ${key} is whitespace-only \u2014 leaving env var unchanged. check your secret value.`
195346
- );
195347
- return null;
195348
- }
195349
- if (key.endsWith("_API_KEY") && hasControlCharacter(trimmed)) {
195350
- log.warning(
195351
- `\xBB ${key} contains a line break or control character and cannot be sent as an HTTP header \u2014 leaving env var unchanged. re-save the secret as a single line.`
195352
- );
195353
- return null;
195354
- }
195355
- if (trimmed !== value2) {
195356
- log.warning(
195357
- `\xBB stripped whitespace from ${key} (whitespace in secret values breaks GitHub Actions log masking)`
195358
- );
195359
- }
195360
- maskSecret(trimmed);
195361
- return trimmed;
195362
- }
195363
- function normalizeEnv() {
195364
- const upperKeys = /* @__PURE__ */ new Map();
195365
- for (const key of Object.keys(process.env)) {
195366
- const upper2 = key.toUpperCase();
195367
- const existing = upperKeys.get(upper2) || [];
195368
- existing.push(key);
195369
- upperKeys.set(upper2, existing);
195370
- }
195371
- for (const [upperKey, keys] of upperKeys) {
195372
- if (keys.length === 1) {
195373
- const key = keys[0];
195374
- if (key !== upperKey) {
195375
- process.env[upperKey] = process.env[key];
195376
- delete process.env[key];
195377
- }
195378
- continue;
195379
- }
195380
- const values = keys.map((k2) => process.env[k2]);
195381
- const uniqueValues = new Set(values);
195382
- if (uniqueValues.size > 1) {
195383
- log.warning(
195384
- `env var conflict: ${keys.join(", ")} have different values. using uppercase ${upperKey}.`
195385
- );
195386
- }
195387
- const preferredKey = keys.find((k2) => k2 === upperKey) || keys[0];
195388
- const preferredValue = process.env[preferredKey];
195389
- for (const key of keys) {
195390
- delete process.env[key];
195391
- }
195392
- process.env[upperKey] = preferredValue;
195393
- }
195394
- for (const key of Object.keys(process.env)) {
195395
- if (!isSensitiveEnvName(key)) continue;
195396
- const value2 = process.env[key];
195397
- if (typeof value2 !== "string" || value2.length === 0) continue;
195398
- const sanitized = sanitizeSecret(key, value2);
195399
- if (sanitized !== null) process.env[key] = sanitized;
195400
- }
195401
- }
195402
-
195403
195710
  // utils/overrides.ts
195404
195711
  var DENIED_OVERRIDE_NAMES = /* @__PURE__ */ new Set([
195405
195712
  "GITHUB_TOKEN",
@@ -195970,7 +196277,6 @@ ${ctx.error}` : ctx.error;
195970
196277
  workflowRun: runId ? { owner: repoContext.owner, repo: repoContext.name, runId } : void 0,
195971
196278
  customParts,
195972
196279
  model: ctx.toolState.model,
195973
- fallbackFrom: ctx.toolState.modelFallback?.from,
195974
196280
  clamped: ctx.toolState.modelClamped,
195975
196281
  unselectedProxyDefault: ctx.toolState.unselectedProxyDefault,
195976
196282
  oss: ctx.toolState.oss,
@@ -196354,7 +196660,8 @@ async function fetchRunContext(params) {
196354
196660
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
196355
196661
  try {
196356
196662
  const headers = {
196357
- Authorization: `Bearer ${params.token}`
196663
+ Authorization: `Bearer ${params.token}`,
196664
+ "X-Pullfrog-Credential-Pools": "1"
196358
196665
  };
196359
196666
  if (params.oidcToken) {
196360
196667
  headers["X-GitHub-OIDC-Token"] = params.oidcToken;
@@ -197651,16 +197958,20 @@ async function main() {
197651
197958
  const preIntrospectionDirty = repoDir ? await dirtyTrackedPaths({ cwd: repoDir }) : null;
197652
197959
  const opencodeCliPath = await agents.opencode.install();
197653
197960
  captureBaselineModels(opencodeCliPath);
197961
+ const injected = [];
197654
197962
  if (runContext.dbSecrets) {
197655
197963
  for (const [key, value2] of Object.entries(runContext.dbSecrets)) {
197656
197964
  if (!process.env[key]) {
197657
197965
  const sanitized = sanitizeSecret(key, value2);
197658
- if (sanitized !== null) process.env[key] = sanitized;
197966
+ if (sanitized === null) continue;
197967
+ process.env[key] = sanitized;
197968
+ injected.push(key);
197659
197969
  }
197660
197970
  }
197661
197971
  const count = Object.keys(runContext.dbSecrets).length;
197662
197972
  if (count > 0) log.info(`\xBB ${count} db secret(s) loaded`);
197663
197973
  }
197974
+ await initializeCredentialPool({ ctx: runContext, slug: payload.model, injected });
197664
197975
  installCodexAuth();
197665
197976
  installXaiAuth();
197666
197977
  captureAuthorizedModels(opencodeCliPath);
@@ -197734,13 +198045,23 @@ async function main() {
197734
198045
  }
197735
198046
  const gitAuthServer = __using(_stack2, await startGitAuthServer(tmpdir3), true);
197736
198047
  setGitAuthServer(gitAuthServer);
198048
+ let resolvedModel = resolveModel({ slug: payload.model });
198049
+ if (!resolvedModel && !payload.proxyModel && runContext.credentialAccess?.candidates.length) {
198050
+ resolvedModel = resolvePoolModel({
198051
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
198052
+ });
198053
+ }
198054
+ let poolSelected = !payload.proxyModel && await selectConfiguredCredential({
198055
+ ctx: runContext,
198056
+ model: resolvedModel
198057
+ });
197737
198058
  const access = decideModelAccess({
197738
198059
  modelExplicit: payload.modelExplicit ?? false,
197739
198060
  model: payload.model,
197740
198061
  oss: runContext.oss,
197741
198062
  proxyActive: !!payload.proxyModel,
197742
198063
  subsidyTarget: runContext.proxyModel,
197743
- resolvedModel: resolveModel({ slug: payload.model }),
198064
+ resolvedModel,
197744
198065
  authorized: getAuthorizedModels()
197745
198066
  });
197746
198067
  if (access.kind === "error") {
@@ -197754,15 +198075,19 @@ async function main() {
197754
198075
  );
197755
198076
  }
197756
198077
  if (access.kind === "proxy") payload.proxyModel = access.target;
197757
- if (access.kind === "byok") payload.proxyModel = void 0;
198078
+ if (access.kind === "byok") {
198079
+ payload.proxyModel = void 0;
198080
+ if (!poolSelected)
198081
+ poolSelected = await selectConfiguredCredential({
198082
+ ctx: runContext,
198083
+ model: resolvedModel
198084
+ });
198085
+ }
197758
198086
  if (runContext.oss && payload.proxyModel) {
197759
198087
  payload.effort = ossEffortFloor({ payload });
197760
198088
  }
197761
- const configuredModel = payload.proxyModel ? void 0 : resolveModel({ slug: payload.model });
197762
- const credentials = payload.proxyModel ? { kind: "ok" } : await checkConfiguredCredentials({
197763
- model: configuredModel,
197764
- authorized: getAuthorizedModels()
197765
- });
198089
+ if (payload.proxyModel) resolvedModel = void 0;
198090
+ const credentials = payload.proxyModel || poolSelected ? { kind: "ok" } : await checkConfiguredCredentials({ model: resolvedModel });
197766
198091
  if (credentials.kind === "dead") {
197767
198092
  throw new Error(
197768
198093
  buildRejectedCredentialError({
@@ -197774,13 +198099,6 @@ async function main() {
197774
198099
  })
197775
198100
  );
197776
198101
  }
197777
- if (credentials.kind === "fellBack" && configuredModel) {
197778
- log.warning(
197779
- `\xBB ${credentials.credential} was rejected by its provider \u2014 running ${credentials.replacement} instead of ${configuredModel}`
197780
- );
197781
- toolState.modelFallback = { from: configuredModel };
197782
- }
197783
- const resolvedModel = credentials.kind === "fellBack" ? credentials.replacement : configuredModel;
197784
198102
  vertexCredentials = materializeVertexCredentials({ model: resolvedModel });
197785
198103
  let agent2 = resolveAgent({
197786
198104
  model: resolvedModel,
@@ -198090,12 +198408,12 @@ ${instructions.user}` : null,
198090
198408
  if (payload.timeout === TIMEOUT_DISABLED) {
198091
198409
  result = await Promise.race([agentPromise, activityTimeout.promise]);
198092
198410
  } else {
198093
- const usable = resolveTimeoutMs(payload.timeout);
198094
- if (payload.timeout && usable === null) {
198411
+ const usable2 = resolveTimeoutMs(payload.timeout);
198412
+ if (payload.timeout && usable2 === null) {
198095
198413
  log.warning(`invalid timeout "${payload.timeout}" (use --notimeout to disable), using 1h`);
198096
198414
  }
198097
- const timeoutMs = usable ?? 36e5;
198098
- const actualTimeout = usable !== null ? payload.timeout : "1h";
198415
+ const timeoutMs = usable2 ?? 36e5;
198416
+ const actualTimeout = usable2 !== null ? payload.timeout : "1h";
198099
198417
  let timeoutId;
198100
198418
  const timeoutPromise = new Promise((_4, reject) => {
198101
198419
  timeoutId = setTimeout(() => {