pullfrog 0.1.89 → 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/cli.mjs +431 -148
- package/dist/index.js +466 -143
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal.js +193 -33
- package/dist/toolState.d.ts +0 -3
- package/dist/utils/buildPullfrogFooter.d.ts +0 -7
- package/dist/utils/claudeSubscription.d.ts +1 -0
- package/dist/utils/credentialFallback.d.ts +8 -16
- package/dist/utils/credentialPool.d.ts +18 -0
- package/dist/utils/openCodeModels.d.ts +4 -0
- package/dist/utils/subscriptionCredentials.d.ts +10 -0
- package/dist/utils/subscriptionProbe.d.ts +18 -2
- package/package.json +1 -1
package/dist/internal/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export { countOutstandingPullfrogThreads } from "../utils/outstandingThreads.ts"
|
|
|
24
24
|
export type { CreateProgressCommentTarget, ProgressComment, ProgressCommentType, } from "../utils/progressComment.ts";
|
|
25
25
|
export { createLeapingProgressComment, deleteProgressCommentApi, getProgressComment, updateProgressComment, } from "../utils/progressComment.ts";
|
|
26
26
|
export { PROVIDER_DASHBOARDS, type ProviderDashboard } from "../utils/providerDashboards.ts";
|
|
27
|
+
export { detectProviderError } from "../utils/providerErrors.ts";
|
|
27
28
|
export type { RunStatusCheckConclusion, RunStatusCheckOctokit, } from "../utils/runStatusCheck.ts";
|
|
28
29
|
export { APPROVAL_CHECK_NAME, createRunStatusCheck, finalizeRunStatusCheck, RUN_STATUS_CHECK_NAME, runStatusCheckNeedsFinalizing, } from "../utils/runStatusCheck.ts";
|
|
29
30
|
export * from "../utils/subscriptionCredentials.ts";
|
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.
|
|
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) {
|
|
@@ -16821,6 +16810,146 @@ var PROVIDER_DASHBOARDS = {
|
|
|
16821
16810
|
}
|
|
16822
16811
|
};
|
|
16823
16812
|
|
|
16813
|
+
// utils/providerErrors.ts
|
|
16814
|
+
var PROVIDER_BILLING_EXHAUSTED_LABEL = "provider billing exhausted";
|
|
16815
|
+
var PROVIDER_NO_ENDPOINTS_LABEL = "provider no routable endpoints";
|
|
16816
|
+
var PROVIDER_USAGE_LIMIT_LABEL = "provider usage limit reached";
|
|
16817
|
+
var statusKey = `\\b(?:status[_ ]?code|http[_ ]?status|status)["']?\\s*[:=]\\s*["']?`;
|
|
16818
|
+
var ANTHROPIC_SPEND_CAP_PATTERN = /reached your specified API usage limits\.?(?:\s*You will regain access on ([^"\n]+?)\.)?/i;
|
|
16819
|
+
var PROVIDER_ERROR_PATTERNS = [
|
|
16820
|
+
// billing-payload patterns come BEFORE bare status-code patterns. providers
|
|
16821
|
+
// commonly return 401 / 429 for billing/quota exhaustion (OpenCode Zen
|
|
16822
|
+
// `CreditsError` / `FreeUsageLimitError`, Gemini `RESOURCE_EXHAUSTED` +
|
|
16823
|
+
// "spending cap", Anthropic "Insufficient balance" / "credit balance is
|
|
16824
|
+
// too low"). these are non-retryable and require user-billing action —
|
|
16825
|
+
// distinct from a transient auth error or rate-limit. status-code patterns
|
|
16826
|
+
// would otherwise win and surface "auth error (401)" / "rate limited (429)"
|
|
16827
|
+
// with no billing hint. see #778, #835.
|
|
16828
|
+
{ regex: /\bCreditsError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16829
|
+
{ regex: /\bFreeUsageLimitError\b/, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16830
|
+
// `balance` is DeepSeek's noun, `credits` is OpenRouter's for the SAME
|
|
16831
|
+
// terminal state — #1164's run 7 (`Insufficient credits. Add more using
|
|
16832
|
+
// …/settings/credits`) matched neither this nor `requires more credits`, so
|
|
16833
|
+
// the one run in that episode that was a genuinely empty wallet rendered with
|
|
16834
|
+
// no CTA at all while runs 1-6 rendered correctly.
|
|
16835
|
+
{ regex: /Insufficient (?:balance|credits)/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16836
|
+
{ regex: /credit balance is too low/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16837
|
+
// "spending cap" (Gemini) and "spending limit" (xAI, #1076) are the same
|
|
16838
|
+
// condition in two house styles — a configured ceiling was reached.
|
|
16839
|
+
{ regex: /spending (?:cap|limit)/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16840
|
+
// xAI exhausts credits and the monthly ceiling in one 403 whose status code
|
|
16841
|
+
// sits nowhere near a `status:` key, so no status pattern below fires (#1076).
|
|
16842
|
+
{ regex: /used all available credits/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16843
|
+
// Gemini's daily / free-tier cap. it tells the user to "check your plan and
|
|
16844
|
+
// billing details" in its own words but matches none of the wallet wordings
|
|
16845
|
+
// above, so it fell to the catch-all `quota` label — which is not the label
|
|
16846
|
+
// the actionable copy is keyed on, so the run rendered a blank comment for a
|
|
16847
|
+
// condition the provider had spelled out (#1114).
|
|
16848
|
+
{
|
|
16849
|
+
regex: /exceeded your current quota, please check your plan and billing/i,
|
|
16850
|
+
label: PROVIDER_BILLING_EXHAUSTED_LABEL
|
|
16851
|
+
},
|
|
16852
|
+
{ regex: /generate_content_free_tier_requests/i, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16853
|
+
// Anthropic's Console spend cap. its noun is "API usage limits", which none of
|
|
16854
|
+
// the wallet-shaped patterns above reach, and its 400 sits nowhere near a
|
|
16855
|
+
// `status:` key either — so the whole message was dropped on the floor (#1208).
|
|
16856
|
+
{ regex: ANTHROPIC_SPEND_CAP_PATTERN, label: PROVIDER_BILLING_EXHAUSTED_LABEL },
|
|
16857
|
+
// OpenCode Zen / Codex-subscription cap: `AI_APICallError: The usage limit has
|
|
16858
|
+
// been reached`. Its noun is "usage limit" with no "rate", no status code and
|
|
16859
|
+
// no wallet wording, so every pattern here missed it and `lastProviderError`
|
|
16860
|
+
// stayed unset — which suppresses nothing, so the run was reported as "The
|
|
16861
|
+
// model produced no output at all … usually transient; re-running often
|
|
16862
|
+
// succeeds" while the same log held nine of these. Measured on a live account
|
|
16863
|
+
// whose ChatGPT subscription was capped.
|
|
16864
|
+
{ regex: /\bthe usage limit has been reached\b/i, label: PROVIDER_USAGE_LIMIT_LABEL },
|
|
16865
|
+
// the OpenRouter shapes `isRouterKeylimitExhaustedError` matches. on a Router
|
|
16866
|
+
// run those mean the PULLFROG wallet is empty and `renderRunError` returns a
|
|
16867
|
+
// `BillingError` before ever reaching this list; on a BYOK run the very same
|
|
16868
|
+
// wire text means the user's OWN OpenRouter wallet is empty, and this entry is
|
|
16869
|
+
// what gives them the right dashboard instead of ours. see #1135.
|
|
16870
|
+
{
|
|
16871
|
+
regex: /requires more credits|Key limit exceeded \(total limit\)/i,
|
|
16872
|
+
label: PROVIDER_BILLING_EXHAUSTED_LABEL
|
|
16873
|
+
},
|
|
16874
|
+
// OpenRouter has no endpoint it is allowed to route to: the customer's
|
|
16875
|
+
// data-policy / guardrail settings exclude every provider serving the model.
|
|
16876
|
+
// dies ~2s into the session with nothing billed, so it is neither a billing
|
|
16877
|
+
// nor an auth condition — and both of those patterns would otherwise be
|
|
16878
|
+
// reached first for a message that mentions neither. see #1164.
|
|
16879
|
+
{
|
|
16880
|
+
regex: /No endpoints available matching your (?:guardrail restrictions|data policy)/i,
|
|
16881
|
+
label: PROVIDER_NO_ENDPOINTS_LABEL
|
|
16882
|
+
},
|
|
16883
|
+
// auth patterns must come BEFORE rate-limit patterns. OpenRouter 401 error
|
|
16884
|
+
// payloads carry `x-ratelimit-*` response headers in the dump, and the
|
|
16885
|
+
// free-form rate-limit regex below would otherwise win on word-boundary
|
|
16886
|
+
// matches inside header names. canonical 401 messages: OpenRouter returns
|
|
16887
|
+
// `{"error":{"message":"User not found","code":401}}` for disabled or
|
|
16888
|
+
// invalid keys (https://openai.luzhipeng.com/docs/api/reference/errors-and-debugging).
|
|
16889
|
+
{ regex: new RegExp(`${statusKey}401\\b`, "i"), label: "auth error (401)" },
|
|
16890
|
+
{ regex: new RegExp(`${statusKey}403\\b`, "i"), label: "auth error (403)" },
|
|
16891
|
+
{ regex: /\bUser not found\b/i, label: "auth error (invalid/disabled key)" },
|
|
16892
|
+
{ regex: /\bInvalid authentication\b/i, label: "auth error (invalid credentials)" },
|
|
16893
|
+
{ regex: /\bNo auth credentials found\b/i, label: "auth error (missing credentials)" },
|
|
16894
|
+
{ regex: new RegExp(`${statusKey}429\\b`, "i"), label: "rate limited (429)" },
|
|
16895
|
+
{ regex: new RegExp(`${statusKey}500\\b`, "i"), label: "provider 500 error" },
|
|
16896
|
+
{ regex: new RegExp(`${statusKey}503\\b`, "i"), label: "provider unavailable (503)" },
|
|
16897
|
+
// matches `rate limit`, `rate limited`, `rate limits exceeded`,
|
|
16898
|
+
// `rate_limit_error`, `rate_limit_exceeded`. the leading `\b` + `[_ ]`
|
|
16899
|
+
// separator rejects `x-ratelimit-*` / `anthropic-ratelimit-*` response
|
|
16900
|
+
// headers (no separator between "rate" and "limit") which routinely
|
|
16901
|
+
// appear in dumped 401 / 4xx error JSON.
|
|
16902
|
+
{ regex: /\brate[_ ]limit/i, label: "rate limited" },
|
|
16903
|
+
{ regex: /\bRESOURCE_EXHAUSTED\b/, label: "quota exhausted" },
|
|
16904
|
+
// Google gRPC `INTERNAL` status. word-boundary anchors reject
|
|
16905
|
+
// `INTERNAL_SERVER_ERROR` (HTTP 500 message that may appear in unrelated
|
|
16906
|
+
// log lines) and identifiers like `INTERNALS`.
|
|
16907
|
+
{ regex: /\bINTERNAL\b/, label: "provider internal error" },
|
|
16908
|
+
{ regex: /\bUNAVAILABLE\b/, label: "provider unavailable" },
|
|
16909
|
+
// matches `quota`, `insufficient_quota`, `quota_exceeded`, `quotaExceeded`.
|
|
16910
|
+
// word-character lookarounds would reject `_quota` / `quotaX`; `quota` is
|
|
16911
|
+
// specific enough that a plain substring match is safe.
|
|
16912
|
+
{ regex: /quota/i, label: "quota error" },
|
|
16913
|
+
// explicit zero-quota response, e.g. `{"limit": 0}`. the `\b` anchor
|
|
16914
|
+
// around `limit` rejects keys like `time_limit` or `field_limit`.
|
|
16915
|
+
{ regex: /["']?\blimit\b["']?\s*:\s*0\b/, label: "zero quota" }
|
|
16916
|
+
];
|
|
16917
|
+
var EXCERPT_MAX_BYTES = 600;
|
|
16918
|
+
var LINES_BEFORE = 1;
|
|
16919
|
+
var LINES_AFTER = 2;
|
|
16920
|
+
function findProviderErrorMatch(text) {
|
|
16921
|
+
for (const entry of PROVIDER_ERROR_PATTERNS) {
|
|
16922
|
+
const m = entry.regex.exec(text);
|
|
16923
|
+
if (!m) continue;
|
|
16924
|
+
return { label: entry.label, excerpt: extractExcerpt(text, m.index) };
|
|
16925
|
+
}
|
|
16926
|
+
return null;
|
|
16927
|
+
}
|
|
16928
|
+
function detectProviderError(text) {
|
|
16929
|
+
return findProviderErrorMatch(text)?.label ?? null;
|
|
16930
|
+
}
|
|
16931
|
+
function extractExcerpt(text, matchIndex) {
|
|
16932
|
+
const lineStart = text.lastIndexOf("\n", matchIndex - 1) + 1;
|
|
16933
|
+
const lineEndRaw = text.indexOf("\n", matchIndex);
|
|
16934
|
+
const lineEnd = lineEndRaw === -1 ? text.length : lineEndRaw;
|
|
16935
|
+
let start = lineStart;
|
|
16936
|
+
for (let i = 0; i < LINES_BEFORE && start > 0; i++) {
|
|
16937
|
+
const prev = text.lastIndexOf("\n", start - 2);
|
|
16938
|
+
start = prev < 0 ? 0 : prev + 1;
|
|
16939
|
+
}
|
|
16940
|
+
let end = lineEnd;
|
|
16941
|
+
for (let i = 0; i < LINES_AFTER && end < text.length; i++) {
|
|
16942
|
+
const next = text.indexOf("\n", end + 1);
|
|
16943
|
+
end = next < 0 ? text.length : next;
|
|
16944
|
+
}
|
|
16945
|
+
let excerpt = text.slice(start, end);
|
|
16946
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) {
|
|
16947
|
+
excerpt = text.slice(lineStart, lineEnd);
|
|
16948
|
+
if (excerpt.length > EXCERPT_MAX_BYTES) excerpt = excerpt.slice(0, EXCERPT_MAX_BYTES);
|
|
16949
|
+
}
|
|
16950
|
+
return excerpt.trim();
|
|
16951
|
+
}
|
|
16952
|
+
|
|
16824
16953
|
// utils/runStatusCheck.ts
|
|
16825
16954
|
var RUN_STATUS_CHECK_NAME = "pullfrog";
|
|
16826
16955
|
var GITHUB_ACTIONS_APP_SLUG = "github-actions";
|
|
@@ -16950,7 +17079,10 @@ var subscriptionSchema = external_exports.object({
|
|
|
16950
17079
|
owner: external_exports.string(),
|
|
16951
17080
|
inherited: external_exports.boolean(),
|
|
16952
17081
|
shared: external_exports.boolean(),
|
|
16953
|
-
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()
|
|
16954
17086
|
});
|
|
16955
17087
|
var subscriptionListSchema = external_exports.object({
|
|
16956
17088
|
subscriptions: external_exports.array(subscriptionSchema),
|
|
@@ -17050,9 +17182,16 @@ var identitySchema = external_exports.object({
|
|
|
17050
17182
|
account_id: external_exports.string().optional(),
|
|
17051
17183
|
email_verified: external_exports.boolean().optional()
|
|
17052
17184
|
});
|
|
17185
|
+
var codexWindowSchema = external_exports.object({ used_percent: external_exports.number().optional(), reset_at: external_exports.number().optional() }).nullish();
|
|
17053
17186
|
var codexUsageSchema = external_exports.object({
|
|
17054
|
-
rate_limit: external_exports.object({
|
|
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()
|
|
17055
17193
|
});
|
|
17194
|
+
var unknown2 = { status: "unknown" };
|
|
17056
17195
|
var SubscriptionCredentialError = class extends Error {
|
|
17057
17196
|
};
|
|
17058
17197
|
async function subscriptionIdentity(input) {
|
|
@@ -17091,12 +17230,16 @@ async function readSubscriptionEndpoint(input) {
|
|
|
17091
17230
|
async function probeSubscription(input) {
|
|
17092
17231
|
if (input.name === "CLAUDE_CODE_OAUTH_TOKEN") {
|
|
17093
17232
|
const result = await preflightClaudeSubscription({ token: input.value, model: input.model });
|
|
17094
|
-
|
|
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 };
|
|
17095
17236
|
}
|
|
17096
17237
|
const codex = input.name === "CODEX_AUTH_JSON" ? parseCodexAuthBody(input.value) : null;
|
|
17097
17238
|
const grok = input.name === "GROK_AUTH_JSON" ? parseXaiAuthBody(input.value) : null;
|
|
17098
17239
|
const auth = codex ?? grok;
|
|
17099
|
-
if (!auth
|
|
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}` };
|
|
17100
17243
|
if (grok && input.model)
|
|
17101
17244
|
return probeInference({
|
|
17102
17245
|
name: "XAI_API_KEY",
|
|
@@ -17109,23 +17252,31 @@ async function probeSubscription(input) {
|
|
|
17109
17252
|
accountId: codex?.tokens.account_id,
|
|
17110
17253
|
grokBilling: !!grok
|
|
17111
17254
|
});
|
|
17112
|
-
if (response?.status === 401) return "rejected";
|
|
17113
|
-
if (!response?.ok) return
|
|
17255
|
+
if (response?.status === 401) return { status: "rejected", detail: "rejected with 401" };
|
|
17256
|
+
if (!response?.ok) return unknown2;
|
|
17114
17257
|
const body = await response.json().catch(() => null);
|
|
17115
|
-
if (codex)
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
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
|
+
};
|
|
17122
17273
|
}
|
|
17123
17274
|
async function probeInference(input) {
|
|
17124
17275
|
const anthropic = input.name === "ANTHROPIC_API_KEY";
|
|
17125
17276
|
const openai = input.name === "OPENAI_API_KEY";
|
|
17126
17277
|
if (!anthropic && !openai && input.name !== "XAI_API_KEY") {
|
|
17127
17278
|
const result = await verifyCredential({ envVar: input.name, value: input.value });
|
|
17128
|
-
return result === "dead" ? "rejected" : result === "alive" ? "usable" :
|
|
17279
|
+
return result === "dead" ? { status: "rejected", detail: "rejected by its provider" } : result === "alive" ? { status: "usable" } : unknown2;
|
|
17129
17280
|
}
|
|
17130
17281
|
const headers = new Headers({ "content-type": "application/json" });
|
|
17131
17282
|
if (anthropic) {
|
|
@@ -17150,12 +17301,20 @@ async function probeInference(input) {
|
|
|
17150
17301
|
).catch(() => null);
|
|
17151
17302
|
if (response?.status === 400 && input.name === "XAI_API_KEY") {
|
|
17152
17303
|
const error48 = external_exports.object({ error: external_exports.string() }).safeParse(await response.json().catch(() => null));
|
|
17153
|
-
return error48.success && error48.data.error.startsWith("Incorrect API key provided.") ? "rejected" : "
|
|
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;
|
|
17154
17309
|
}
|
|
17155
17310
|
await response?.body?.cancel();
|
|
17156
|
-
if (!response) return
|
|
17157
|
-
if (response.ok) return "usable";
|
|
17158
|
-
|
|
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;
|
|
17159
17318
|
}
|
|
17160
17319
|
|
|
17161
17320
|
// utils/time.ts
|
|
@@ -17214,6 +17373,7 @@ export {
|
|
|
17214
17373
|
decodeJwtExpMs,
|
|
17215
17374
|
defaultAutoTier,
|
|
17216
17375
|
deleteProgressCommentApi,
|
|
17376
|
+
detectProviderError,
|
|
17217
17377
|
finalizeRunStatusCheck,
|
|
17218
17378
|
getAutoSelectHintModel,
|
|
17219
17379
|
getModelEffortLevels,
|
package/dist/toolState.d.ts
CHANGED
|
@@ -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
|
|
@@ -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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
* seconds in against a "rotate your GitHub Actions secret"
|
|
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<
|
|
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<
|
|
38
|
+
}): Promise<ProbeVerdict>;
|