HeyPuter/puter · error · HttpError

insufficient_funds

insufficient_funds

Error message

Insufficient credits

What it means

Same metering pattern as the voice driver: estimatedSeconds = Math.ceil(byteLength/16000), estimatedCost = SPEECH_TO_TEXT_COSTS['openai:<model>:second'] * seconds. If metering.hasEnoughCredits returns false, the provider throws 402 / insufficient_funds before the OpenAI call. Usage is incremented post-call by the same estimate. Costs are defined per model in costs.ts.

Source

Thrown at src/backend/drivers/ai-speech2txt/providers/openai/OpenAISpeechToTextProvider.ts:223

            );
        }

        // Estimate seconds from raw bytes — 16 kbps is a conservative speech-audio
        // lower bound. Full metadata parsing (music-metadata) is deferred — clients
        // aren't observably sensitive to billing-time delta vs real duration.
        const estimatedSeconds = Math.max(
            1,
            Math.ceil(loaded.buffer.byteLength / 16000),
        );
        const usageType = `openai:${selectedModel}:second`;
        const ucentsPerSecond = SPEECH_TO_TEXT_COSTS[usageType] ?? 0;
        const estimatedCost = ucentsPerSecond * estimatedSeconds;
        const allowed = await this.deps.metering.hasEnoughCredits(
            actor,
            estimatedCost,
        );
        if (!allowed)
            throw new HttpError(402, 'Insufficient credits', {
                legacyCode: 'insufficient_funds',
            });

        const openaiFile = await toFile(
            loaded.buffer,
            loaded.filename,
            loaded.mimeType ? { type: loaded.mimeType } : undefined,
        );

        const payload: Record<string, unknown> = {
            file: openaiFile,
            model: selectedModel,
        };
        if (args.response_format)
            payload.response_format = args.response_format;
        if (args.language) payload.language = args.language;
        if (typeof args.temperature === 'number')
            payload.temperature = args.temperature;

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Top up credits.
  2. Shorten or compress the audio to cut the byte-based duration estimate (and real cost).
  3. Pre-check balance and show the estimated cost to the user before submit.
Defensive patterns

Strategy: validation

Validate before calling

// mirror costs.ts: microcents per second per model
const PER_SEC_UCENTS = { 'gpt-4o-transcribe':10000,'gpt-4o-mini-transcribe':5000,'gpt-4o-transcribe-diarize':10000,'whisper-1':10000 };
const secs = Math.max(1, Math.ceil(fileBytes / 16000));
if (userBalanceUcents < secs * PER_SEC_UCENTS[model]) {
  throw new Error('insufficient credits — top up');
}

Try / catch

try {
  await driver.transcribe(args);
} catch (e) {
  if (e?.legacyCode === 'insufficient_funds' || e?.status === 402) {
    showTopUpPrompt();
  } else throw e;
}

Prevention

When it happens

Trigger: A low-balance user transcribing a long clip; a 25 MB max-size clip producing a large byte-based estimate.

Common situations: Free-tier users; verbose_json on a long recording; org budgets exhausted.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/fdc469223ec270ae. Report an issue: GitHub.