HeyPuter/puter · error · HttpError
insufficient_funds
insufficient_funds
Error message
Insufficient funds
What it means
Credit gate in GeminiTTSProvider.synthesize. It estimates input tokens (~chars/4) and output audio tokens (~150 wpm, 25 tokens/sec), converts to microcents via the model's input/output_audio rates, and rejects with HTTP 402 (legacyCode insufficient_funds) when meteringService.hasEnoughCredits(actor, estimate) is false. Because the estimate is heuristic, actual metered usage may differ.
Source
Thrown at src/backend/drivers/ai-tts/providers/gemini/GeminiTTSProvider.ts:220
const estimatedInputTokens = Math.max(1, Math.ceil(text.length / 4));
const wordCount = text.split(/\s+/).length;
const estimatedDurationSec = Math.max(1, (wordCount / 150) * 60);
const estimatedOutputTokens = Math.ceil(estimatedDurationSec * 25);
const estimatedInputCostCents =
(estimatedInputTokens / 1_000_000) * costs.input;
const estimatedOutputCostCents =
(estimatedOutputTokens / 1_000_000) * costs.output_audio;
const estimatedTotalMicroCents = this.#toMicroCents(
estimatedInputCostCents + estimatedOutputCostCents,
);
const usageAllowed = await this.meteringService.hasEnoughCredits(
actor,
estimatedTotalMicroCents,
);
if (!usageAllowed) {
throw new HttpError(402, 'Insufficient funds', {
legacyCode: 'insufficient_funds',
});
}
// The TTS models require the text to be framed as a transcript
// to read aloud. Prefixing with "Say:" prevents the model from
// trying to generate conversational text instead of audio.
const inputText = instructions
? `${instructions}\n\nSay the following text aloud:\n${text}`
: `Say the following text aloud:\n${text}`;
// Let Google GenAI `ApiError`s bubble — they carry `.status` and
// are mapped to `upstream_*` HttpErrors by the driver-boundary
// translator. Catching here and wrapping as 502 hid the upstream
// status and caused 4xx validation errors to page.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response: any = await this.#client.models.generateContent({
model,View on GitHub (pinned to 908ec23eda)
Solutions
- Top up the actor/workspace balance.
- Shorten the text or split into smaller requests.
- Use the Flash model (cheaper) instead of Pro.
- Note the estimate is conservative; retry after a small top-up may succeed even for the same text.
Defensive patterns
Strategy: try-catch
Validate before calling
// Rough client-side estimate mirroring the server heuristic.
function estimateGeminiUcents(text, modelCosts) {
const inputTokens = Math.max(1, Math.ceil(text.length / 4));
const words = text.split(/\s+/).length;
const durSec = Math.max(1, (words / 150) * 60);
const outputTokens = Math.ceil(durSec * 25);
const centsIn = (inputTokens / 1_000_000) * modelCosts.input;
const centsOut = (outputTokens / 1_000_000) * modelCosts.output_audio;
return Math.ceil((centsIn + centsOut) * 1_000_000); // microcents
}
// Compare to balance; note the server estimate is conservative. Try / catch
try {
await driver.synthesize({ text, provider: 'gemini', model });
} catch (e) {
if (e?.status === 402 || e?.fields?.legacyCode === 'insufficient_funds') {
// prompt top-up, shorten text, or switch Flash->same text after balance refresh
} else throw e;
} Prevention
- Use the Flash model for long text; it's cheaper than Pro.
- The estimate is conservative — a small top-up may unblock the same text.
- Show an estimated cost in the UI before submission.
When it happens
Trigger: An actor whose balance is below the estimated combined input+output cost for the text on the chosen Gemini TTS model. Long text drives both input and output estimates up.
Common situations: Long narration on a Pro model exhausting a small balance; free-tier users; the estimate being conservative so a request is rejected even though actual cost would have fit.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/34713b5b634a6c8a.
Report an issue: GitHub.