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/internal.js CHANGED
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "pullfrog",
11
- version: "0.1.90",
11
+ version: "0.1.91",
12
12
  cliContractVersion: "1",
13
13
  type: "module",
14
14
  bin: {
@@ -16246,15 +16246,6 @@ function commercialPaywallBody(params) {
16246
16246
  // utils/buildPullfrogFooter.ts
16247
16247
  var PULLFROG_DIVIDER = "<!-- PULLFROG_DIVIDER_DO_NOT_REMOVE_PLZ -->";
16248
16248
  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>`;
16249
- function providerDisplayName(slug) {
16250
- try {
16251
- const key = getModelProvider(slug);
16252
- const meta3 = providers[key];
16253
- return meta3?.displayName ?? key;
16254
- } catch {
16255
- return slug;
16256
- }
16257
- }
16258
16249
  function formatModelLabel(params) {
16259
16250
  const alias = resolveDisplayAlias(params.model) ?? // reverse-lookup: when the caller passes an effective model (proxy or
16260
16251
  // resolved target like "openrouter/anthropic/claude-opus-4.7") instead of
@@ -16269,9 +16260,6 @@ function formatModelLabel(params) {
16269
16260
  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)`;
16270
16261
  }
16271
16262
  const base = alias?.isFree ? `\`${displayName}\` (free)` : `\`${displayName}\``;
16272
- if (params.fallbackFrom) {
16273
- return `${base} (credentials for ${providerDisplayName(params.fallbackFrom)} were rejected by the provider)`;
16274
- }
16275
16263
  if (params.clamped?.reason === "trial") {
16276
16264
  return `${base} (model usage covered by Pullfrog)`;
16277
16265
  }
@@ -16312,7 +16300,6 @@ function buildPullfrogFooter(params) {
16312
16300
  parts.push(
16313
16301
  `Using ${formatModelLabel({
16314
16302
  model: params.model,
16315
- fallbackFrom: params.fallbackFrom,
16316
16303
  clamped: params.clamped,
16317
16304
  unselectedProxyDefault: params.unselectedProxyDefault,
16318
16305
  oss: params.oss
@@ -16480,10 +16467,12 @@ async function preflightClaudeSubscription(params) {
16480
16467
  if (res.status !== 401 && res.status !== 403 && res.status !== 429)
16481
16468
  return { usable: true, status: res.status };
16482
16469
  const body = await res.text().catch(() => "");
16470
+ const reset = Number(res.headers.get("anthropic-ratelimit-unified-reset"));
16483
16471
  return {
16484
16472
  usable: false,
16485
16473
  status: res.status,
16486
- reason: `${res.status}: ${extractApiErrorMessage(body)}`
16474
+ reason: `${res.status}: ${extractApiErrorMessage(body)}`,
16475
+ resetAt: res.status === 429 && reset > 0 ? new Date(reset * 1e3) : void 0
16487
16476
  };
16488
16477
  }
16489
16478
  function extractApiErrorMessage(body) {
@@ -17090,7 +17079,10 @@ var subscriptionSchema = external_exports.object({
17090
17079
  owner: external_exports.string(),
17091
17080
  inherited: external_exports.boolean(),
17092
17081
  shared: external_exports.boolean(),
17093
- registeredAt: external_exports.string()
17082
+ registeredAt: external_exports.string(),
17083
+ /** the provider's last verdict on the current value; null = usable or never checked. */
17084
+ status: external_exports.enum(["rejected", "exhausted"]).nullish(),
17085
+ resetAt: external_exports.string().nullish()
17094
17086
  });
17095
17087
  var subscriptionListSchema = external_exports.object({
17096
17088
  subscriptions: external_exports.array(subscriptionSchema),
@@ -17190,9 +17182,16 @@ var identitySchema = external_exports.object({
17190
17182
  account_id: external_exports.string().optional(),
17191
17183
  email_verified: external_exports.boolean().optional()
17192
17184
  });
17185
+ var codexWindowSchema = external_exports.object({ used_percent: external_exports.number().optional(), reset_at: external_exports.number().optional() }).nullish();
17193
17186
  var codexUsageSchema = external_exports.object({
17194
- rate_limit: external_exports.object({ allowed: external_exports.boolean(), limit_reached: external_exports.boolean() }).optional()
17187
+ rate_limit: external_exports.object({
17188
+ allowed: external_exports.boolean(),
17189
+ limit_reached: external_exports.boolean(),
17190
+ primary_window: codexWindowSchema,
17191
+ secondary_window: codexWindowSchema
17192
+ }).optional()
17195
17193
  });
17194
+ var unknown2 = { status: "unknown" };
17196
17195
  var SubscriptionCredentialError = class extends Error {
17197
17196
  };
17198
17197
  async function subscriptionIdentity(input) {
@@ -17231,12 +17230,16 @@ async function readSubscriptionEndpoint(input) {
17231
17230
  async function probeSubscription(input) {
17232
17231
  if (input.name === "CLAUDE_CODE_OAUTH_TOKEN") {
17233
17232
  const result = await preflightClaudeSubscription({ token: input.value, model: input.model });
17234
- return result.usable ? "usable" : "rejected";
17233
+ if (result.usable)
17234
+ return result.status !== void 0 && result.status < 300 ? { status: "usable" } : unknown2;
17235
+ return result.status === 429 ? { status: "exhausted", detail: result.reason, resetAt: result.resetAt } : { status: "rejected", detail: result.reason };
17235
17236
  }
17236
17237
  const codex = input.name === "CODEX_AUTH_JSON" ? parseCodexAuthBody(input.value) : null;
17237
17238
  const grok = input.name === "GROK_AUTH_JSON" ? parseXaiAuthBody(input.value) : null;
17238
17239
  const auth = codex ?? grok;
17239
- if (!auth || auth.refresh_rejected_at) return "rejected";
17240
+ if (!auth) return { status: "rejected", detail: "the stored credential is malformed" };
17241
+ if (auth.refresh_rejected_at)
17242
+ return { status: "rejected", detail: `refresh rejected at ${auth.refresh_rejected_at}` };
17240
17243
  if (grok && input.model)
17241
17244
  return probeInference({
17242
17245
  name: "XAI_API_KEY",
@@ -17249,23 +17252,31 @@ async function probeSubscription(input) {
17249
17252
  accountId: codex?.tokens.account_id,
17250
17253
  grokBilling: !!grok
17251
17254
  });
17252
- if (response?.status === 401) return "rejected";
17253
- if (!response?.ok) return "unknown";
17255
+ if (response?.status === 401) return { status: "rejected", detail: "rejected with 401" };
17256
+ if (!response?.ok) return unknown2;
17254
17257
  const body = await response.json().catch(() => null);
17255
- if (codex) {
17256
- const parsed = codexUsageSchema.safeParse(body);
17257
- const limit = parsed.success ? parsed.data.rate_limit : void 0;
17258
- if (!limit) return "unknown";
17259
- return limit.allowed && !limit.limit_reached ? "usable" : "rejected";
17260
- }
17261
- return "unknown";
17258
+ if (!codex) return unknown2;
17259
+ const parsed = codexUsageSchema.safeParse(body);
17260
+ const limit = parsed.success ? parsed.data.rate_limit : void 0;
17261
+ if (!limit) return unknown2;
17262
+ if (limit.allowed && !limit.limit_reached) return { status: "usable" };
17263
+ const windows = [limit.primary_window, limit.secondary_window];
17264
+ const full = windows.filter((window) => (window?.used_percent ?? 0) >= 100);
17265
+ const resets = (full.length ? full : windows).flatMap(
17266
+ (window) => window?.reset_at ? [window.reset_at] : []
17267
+ );
17268
+ return {
17269
+ status: "exhausted",
17270
+ detail: "the ChatGPT plan's usage limit is reached",
17271
+ resetAt: resets.length ? new Date(Math.max(...resets) * 1e3) : void 0
17272
+ };
17262
17273
  }
17263
17274
  async function probeInference(input) {
17264
17275
  const anthropic = input.name === "ANTHROPIC_API_KEY";
17265
17276
  const openai = input.name === "OPENAI_API_KEY";
17266
17277
  if (!anthropic && !openai && input.name !== "XAI_API_KEY") {
17267
17278
  const result = await verifyCredential({ envVar: input.name, value: input.value });
17268
- return result === "dead" ? "rejected" : result === "alive" ? "usable" : "unknown";
17279
+ return result === "dead" ? { status: "rejected", detail: "rejected by its provider" } : result === "alive" ? { status: "usable" } : unknown2;
17269
17280
  }
17270
17281
  const headers = new Headers({ "content-type": "application/json" });
17271
17282
  if (anthropic) {
@@ -17290,12 +17301,20 @@ async function probeInference(input) {
17290
17301
  ).catch(() => null);
17291
17302
  if (response?.status === 400 && input.name === "XAI_API_KEY") {
17292
17303
  const error48 = external_exports.object({ error: external_exports.string() }).safeParse(await response.json().catch(() => null));
17293
- return error48.success && error48.data.error.startsWith("Incorrect API key provided.") ? "rejected" : "unknown";
17304
+ return error48.success && error48.data.error.startsWith("Incorrect API key provided.") ? { status: "rejected", detail: "rejected with 400: incorrect API key" } : unknown2;
17305
+ }
17306
+ if (response?.status === 429 && openai) {
17307
+ const error48 = external_exports.object({ error: external_exports.object({ code: external_exports.string() }) }).safeParse(await response.json().catch(() => null));
17308
+ return error48.success && error48.data.error.code === "insufficient_quota" ? { status: "exhausted", detail: "refused with 429: insufficient quota", resetAt: void 0 } : unknown2;
17294
17309
  }
17295
17310
  await response?.body?.cancel();
17296
- if (!response) return "unknown";
17297
- if (response.ok) return "usable";
17298
- return [401, 402, 403, 429].includes(response.status) ? "rejected" : "unknown";
17311
+ if (!response) return unknown2;
17312
+ if (response.ok) return { status: "usable" };
17313
+ if (response.status === 401 || response.status === 403)
17314
+ return { status: "rejected", detail: `rejected with ${response.status}` };
17315
+ if (response.status === 402)
17316
+ return { status: "exhausted", detail: "refused with 402", resetAt: void 0 };
17317
+ return unknown2;
17299
17318
  }
17300
17319
 
17301
17320
  // utils/time.ts
@@ -138,9 +138,6 @@ export interface ToolState {
138
138
  model?: string | undefined;
139
139
  agent?: string | undefined;
140
140
  credential?: AgentCredential | undefined;
141
- modelFallback?: {
142
- from: string;
143
- } | undefined;
144
141
  unselectedProxyDefault?: boolean | undefined;
145
142
  modelClamped?: {
146
143
  from: string;
@@ -17,13 +17,6 @@ export interface BuildPullfrogFooterParams {
17
17
  customParts?: string[] | undefined;
18
18
  /** model slug from payload (e.g., "anthropic/claude-opus"). shown in footer as "Using `Model Name`" */
19
19
  model?: string | undefined;
20
- /**
21
- * When a credential was rejected and the run moved to another model, this is
22
- * the slug the user had configured (e.g. "anthropic/claude-opus") — the footer
23
- * renders `Using <model> (credentials for <configured> were rejected by the
24
- * provider)` so the substitution is visible in PR comments + reviews.
25
- */
26
- fallbackFrom?: string | undefined;
27
20
  /**
28
21
  * When a Router account had a model (or the intelligent tier) selected that
29
22
  * the server clamped to the efficient default — custom picks are card-gated
@@ -12,6 +12,7 @@ export type SubscriptionPreflight = {
12
12
  usable: false;
13
13
  status: number;
14
14
  reason: string;
15
+ resetAt: Date | undefined;
15
16
  };
16
17
  /**
17
18
  * preflight a Claude subscription OAuth token (`CLAUDE_CODE_OAUTH_TOKEN`)
@@ -1,20 +1,13 @@
1
1
  /**
2
2
  * `ok` covers "the credential works", "we couldn't tell", and "something else
3
3
  * on this model still works" — the run proceeds as it did before this check
4
- * existed. The other two are only reached when every credential the configured
5
- * model could use has been explicitly rejected by its own provider.
6
- *
7
- * `replacement` is a concrete model, never a "just auto-select something":
8
- * deciding an alternative exists and picking it have to be the SAME decision,
9
- * or the picker can land back on the provider that just failed.
4
+ * existed. `dead` is only reached when every credential the configured model
5
+ * could use has been explicitly rejected by its own provider. A run never moves
6
+ * to a different model: the pool of credentials for THIS provider is where a
7
+ * rejected one is routed around (see wiki/subscription-credentials.md).
10
8
  */
11
9
  export type CredentialOutcome = {
12
10
  kind: "ok";
13
- } | {
14
- kind: "fellBack";
15
- credential: string;
16
- reason: string | undefined;
17
- replacement: string;
18
11
  } | {
19
12
  kind: "dead";
20
13
  credential: string;
@@ -22,10 +15,10 @@ export type CredentialOutcome = {
22
15
  };
23
16
  /**
24
17
  * Ask the providers whether the credentials behind the configured model still
25
- * work, BEFORE the agent spawns — so a rejected one becomes either a run on a
26
- * model that does work or an accurate error, instead of the agent 401ing three
27
- * seconds in against a "rotate your GitHub Actions secret" CTA naming a place
28
- * the user never put a key.
18
+ * work, BEFORE the agent spawns — so a rejected one becomes either a run on
19
+ * another credential for the same model or an accurate error, instead of the
20
+ * agent 401ing three seconds in against a "rotate your GitHub Actions secret"
21
+ * CTA naming a place the user never put a key.
29
22
  *
30
23
  * Failed credentials are deleted from `process.env` only when this run can
31
24
  * still proceed without them. Nothing downstream infers anything from that
@@ -38,5 +31,4 @@ export type CredentialOutcome = {
38
31
  */
39
32
  export declare function checkConfiguredCredentials(params: {
40
33
  model: string | undefined;
41
- authorized: Set<string>;
42
34
  }): Promise<CredentialOutcome>;
@@ -0,0 +1,18 @@
1
+ import type { RunContextData } from "./runContextData.ts";
2
+ /**
3
+ * load one subscription per provider for model discovery; keep the pool unflattened. runs
4
+ * after the account secrets are injected: `injected` names the ones that came from storage,
5
+ * so everything else set here is a workflow credential.
6
+ */
7
+ export declare function initializeCredentialPool(input: {
8
+ ctx: RunContextData;
9
+ slug: string | undefined;
10
+ injected: string[];
11
+ }): Promise<void>;
12
+ export declare function resolvePoolModel(input: {
13
+ codexAgent: boolean;
14
+ }): string | undefined;
15
+ export declare function selectConfiguredCredential(input: {
16
+ ctx: RunContextData;
17
+ model: string | undefined;
18
+ }): Promise<boolean>;
@@ -14,6 +14,10 @@ export declare function captureBaselineModels(cliPath: string): void;
14
14
  * Codex auth.json are in place. Logs the diff against the baseline as
15
15
  * `» BYOK auth enabled N model(s): …`. */
16
16
  export declare function captureAuthorizedModels(cliPath: string): void;
17
+ /** A model whose credential the pool just probed. Added directly: a second
18
+ * `opencode models` would run inside the checkout and load the repo's own
19
+ * `.opencode` plugins, which can outlast its timeout and empty the set. */
20
+ export declare function authorizeModel(model: string): void;
17
21
  /** Authorized set captured after Pullfrog-stored auth is applied. Throws if
18
22
  * called before `captureAuthorizedModels` — the call sites (api-key
19
23
  * validation, auto-select) all run strictly after capture. */
@@ -18,6 +18,11 @@ export declare const subscriptionSchema: z.ZodObject<{
18
18
  inherited: z.ZodBoolean;
19
19
  shared: z.ZodBoolean;
20
20
  registeredAt: z.ZodString;
21
+ status: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
22
+ rejected: "rejected";
23
+ exhausted: "exhausted";
24
+ }>>>;
25
+ resetAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
26
  }, z.core.$strip>;
22
27
  export type Subscription = z.infer<typeof subscriptionSchema>;
23
28
  export declare const subscriptionListSchema: z.ZodObject<{
@@ -34,6 +39,11 @@ export declare const subscriptionListSchema: z.ZodObject<{
34
39
  inherited: z.ZodBoolean;
35
40
  shared: z.ZodBoolean;
36
41
  registeredAt: z.ZodString;
42
+ status: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
43
+ rejected: "rejected";
44
+ exhausted: "exhausted";
45
+ }>>>;
46
+ resetAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
37
47
  }, z.core.$strip>>;
38
48
  writable: z.ZodBoolean;
39
49
  }, z.core.$strip>;
@@ -1,4 +1,20 @@
1
1
  import type { SubscriptionName } from "./subscriptionCredentials.ts";
2
+ /**
3
+ * the provider's answer about one credential. only `rejected` and `exhausted`
4
+ * advance the chain; `unknown` never changes who pays.
5
+ */
6
+ export type ProbeVerdict = {
7
+ status: "usable";
8
+ } | {
9
+ status: "unknown";
10
+ } | {
11
+ status: "rejected";
12
+ detail: string;
13
+ } | {
14
+ status: "exhausted";
15
+ detail: string;
16
+ resetAt: Date | undefined;
17
+ };
2
18
  export declare class SubscriptionCredentialError extends Error {
3
19
  }
4
20
  export declare function subscriptionIdentity(input: {
@@ -13,10 +29,10 @@ export declare function probeSubscription(input: {
13
29
  name: SubscriptionName;
14
30
  value: string;
15
31
  model?: string | undefined;
16
- }): Promise<"usable" | "rejected" | "unknown">;
32
+ }): Promise<ProbeVerdict>;
17
33
  /** probe the chosen model, not a models-list permission a restricted key may lack. */
18
34
  export declare function probeInference(input: {
19
35
  name: string;
20
36
  value: string;
21
37
  model: string;
22
- }): Promise<"usable" | "rejected" | "unknown">;
38
+ }): Promise<ProbeVerdict>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "cliContractVersion": "1",
5
5
  "type": "module",
6
6
  "bin": {