router: decouple per-run key budget from wallet, add overdraft buffer (#616)
Replaces today's `keyLimitUsd = min(walletBalance, $25)` with population-aware buffers so users can use 100% of their credits before being paywalled, and opaque mid-run "more credits" failures (e.g. https://github.com/pullfrog/app/actions/runs/25531633203) get a clear PR comment instead of a generic stack-trace dump. Policy matrix: - Auto-reload accounts: `wallet + autoReloadAmountCents` (default $50, no cap) - Card + no-autoreload: `wallet + $5` overdraft buffer - No card: `wallet` (no buffer; existing zero-balance 402 stays) - OSS: `$10` (unchanged) Removes the $25 per-run cap entirely. Long Build runs at high-balance accounts no longer silently cap at $25. Other changes: - Classify mid-run OpenRouter "requires more credits, or fewer max_tokens" errors as `router_keylimit_exhausted` BillingError so users get an actionable PR comment. - Override OpenCode `max_tokens: 32000` default to `5000` via OpenCodeConfig.limit.output. Drops Opus per-call upfront budget reservation from ~$2.40 to ~$0.38 — what makes low-wallet runs viable at all. - Switch `findInitialComment` and `findExistingPaywallComment` to GraphQL `issueOrPullRequest(number:) { comments(last: 100) }` (single round trip, actually returns newest-100; REST listComments doesn't support sort/direction). Also fixes a latent `comments.find()` returning the OLDEST match instead of the most recent — now selects max(databaseId). - Wrap `syncAccountUsage` in `prisma.$transaction` with `SELECT ... FOR UPDATE` on the account row. Pre/post-balance reads inside the transaction enable deterministic low-balance edge detection (currently logs; will push the outreach.low_balance task once #592 lands). Plan: .cursor/plans/router-low-balance-paywall.plan.md (in companion wiki-billing branch)
This commit is contained in:
committed by
pullfrog[bot]
parent
9d04cad360
commit
4101df566b
@@ -1,4 +1,4 @@
|
||||
import { detectProviderError } from "./providerErrors.ts";
|
||||
import { detectProviderError, isRouterKeylimitExhaustedError } from "./providerErrors.ts";
|
||||
|
||||
describe("detectProviderError", () => {
|
||||
describe("false positives previously seen in production", () => {
|
||||
@@ -78,3 +78,39 @@ describe("detectProviderError", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isRouterKeylimitExhaustedError", () => {
|
||||
it("matches the canonical OpenRouter mid-run error", () => {
|
||||
expect(
|
||||
isRouterKeylimitExhaustedError(
|
||||
"APIError: This request requires more credits, or fewer max_tokens. " +
|
||||
"You requested up to 32000 tokens, but can only afford 22800. " +
|
||||
"To increase, visit https://openrouter.ai/settings/keys and create a key with a higher total limit"
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches the 'requires more credits' phrasing on its own", () => {
|
||||
expect(
|
||||
isRouterKeylimitExhaustedError("This request requires more credits, or fewer max_tokens.")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("matches the 'requested up to ... can only afford' phrasing on its own", () => {
|
||||
expect(
|
||||
isRouterKeylimitExhaustedError("You requested up to 8000 tokens but can only afford 1234")
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not match generic out-of-credit text", () => {
|
||||
expect(isRouterKeylimitExhaustedError("Your account has insufficient credits")).toBe(false);
|
||||
expect(isRouterKeylimitExhaustedError("rate_limit_exceeded")).toBe(false);
|
||||
expect(isRouterKeylimitExhaustedError('{"limit": 0}')).toBe(false);
|
||||
});
|
||||
|
||||
it("does not match unrelated mentions of max_tokens", () => {
|
||||
expect(isRouterKeylimitExhaustedError("max_tokens parameter must be a positive integer")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,3 +36,24 @@ export function detectProviderError(text: string): string | null {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenRouter's response when the per-run key's remaining budget can't cover
|
||||
* the agent's `max_tokens` reservation. Distinct from a generic provider error
|
||||
* because it's a Pullfrog billing concern, not an upstream outage — the user's
|
||||
* Router wallet ran out (or the key budget was undersized at mint time and the
|
||||
* agent ran out of headroom partway through).
|
||||
*
|
||||
* Match must be specific to this exact OpenRouter error class. Generic "credits"
|
||||
* or "limit" text shows up in unrelated errors and would mis-classify them.
|
||||
*
|
||||
* Sample:
|
||||
* `APIError: This request requires more credits, or fewer max_tokens.
|
||||
* You requested up to 32000 tokens, but can only afford 22800.`
|
||||
*/
|
||||
const ROUTER_KEYLIMIT_EXHAUSTED_PATTERN =
|
||||
/requires more credits.*?fewer max_tokens|requested up to \d+ tokens.*?can only afford/i;
|
||||
|
||||
export function isRouterKeylimitExhaustedError(text: string): boolean {
|
||||
return ROUTER_KEYLIMIT_EXHAUSTED_PATTERN.test(text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user