{"record":{"id":"fdc469223ec270ae","repo":"HeyPuter/puter","slug":"insufficient-funds-fdc469","errorCode":"insufficient_funds","errorMessage":"Insufficient credits","messagePattern":"Insufficient credits","errorType":"http","errorClass":"HttpError","httpStatus":402,"severity":"error","filePath":"src/backend/drivers/ai-speech2txt/providers/openai/OpenAISpeechToTextProvider.ts","lineNumber":223,"sourceCode":"            );\n        }\n\n        // Estimate seconds from raw bytes — 16 kbps is a conservative speech-audio\n        // lower bound. Full metadata parsing (music-metadata) is deferred — clients\n        // aren't observably sensitive to billing-time delta vs real duration.\n        const estimatedSeconds = Math.max(\n            1,\n            Math.ceil(loaded.buffer.byteLength / 16000),\n        );\n        const usageType = `openai:${selectedModel}:second`;\n        const ucentsPerSecond = SPEECH_TO_TEXT_COSTS[usageType] ?? 0;\n        const estimatedCost = ucentsPerSecond * estimatedSeconds;\n        const allowed = await this.deps.metering.hasEnoughCredits(\n            actor,\n            estimatedCost,\n        );\n        if (!allowed)\n            throw new HttpError(402, 'Insufficient credits', {\n                legacyCode: 'insufficient_funds',\n            });\n\n        const openaiFile = await toFile(\n            loaded.buffer,\n            loaded.filename,\n            loaded.mimeType ? { type: loaded.mimeType } : undefined,\n        );\n\n        const payload: Record<string, unknown> = {\n            file: openaiFile,\n            model: selectedModel,\n        };\n        if (args.response_format)\n            payload.response_format = args.response_format;\n        if (args.language) payload.language = args.language;\n        if (typeof args.temperature === 'number')\n            payload.temperature = args.temperature;","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/HeyPuter/puter/blob/908ec23eda38526170322c3edf71ba45ecb1ca95/src/backend/drivers/ai-speech2txt/providers/openai/OpenAISpeechToTextProvider.ts#L205-L241","documentation":"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.","triggerScenarios":"A low-balance user transcribing a long clip; a 25 MB max-size clip producing a large byte-based estimate.","commonSituations":"Free-tier users; verbose_json on a long recording; org budgets exhausted.","solutions":["Top up credits.","Shorten or compress the audio to cut the byte-based duration estimate (and real cost).","Pre-check balance and show the estimated cost to the user before submit."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// mirror costs.ts: microcents per second per model\nconst PER_SEC_UCENTS = { 'gpt-4o-transcribe':10000,'gpt-4o-mini-transcribe':5000,'gpt-4o-transcribe-diarize':10000,'whisper-1':10000 };\nconst secs = Math.max(1, Math.ceil(fileBytes / 16000));\nif (userBalanceUcents < secs * PER_SEC_UCENTS[model]) {\n  throw new Error('insufficient credits — top up');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await driver.transcribe(args);\n} catch (e) {\n  if (e?.legacyCode === 'insufficient_funds' || e?.status === 402) {\n    showTopUpPrompt();\n  } else throw e;\n}","preventionTips":["Show the estimated cost from byte size before upload.","Check balance before starting transcription.","Trim/compress audio to lower the byte-based estimate."],"tags":["billing","credits","speech2txt","openai"],"backgroundTag":null,"analyzedSha":"908ec23eda38526170322c3edf71ba45ecb1ca95","analyzedAt":"2026-08-12T20:53:15.911Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}