HeyPuter/puter · error · HttpError
field_invalid
field_invalid
Error message
Invalid model: ${modelId}. Expected: ${expected.join(', ')} What it means
ElevenLabsTTSProvider gates the model on the cost table, not the advertised model list: if modelId (after defaulting to eleven_multilingual_v2) is not a key in ELEVENLABS_TTS_COSTS, it throws HTTP 400 (legacyCode field_invalid) with fields.key='model', fields.expected, and fields.got. Rationale (per source comment): an id we cannot price is an id we cannot bill for, but the vendor still bills.
Source
Thrown at src/backend/drivers/ai-tts/providers/elevenlabs/ElevenLabsTTSProvider.ts:225
return { url: SAMPLE_AUDIO_URL, content_type: 'audio' };
}
if (typeof text !== 'string' || !text.trim()) {
throw new HttpError(400, 'Missing required field: text', {
legacyCode: 'field_required',
fields: { key: 'text' },
});
}
const voiceId = voiceArg || this.defaultVoiceId;
const modelId = modelArg || DEFAULT_MODEL;
// Gate on the cost table rather than the advertised model list: an id
// we can't price is an id we can't bill for, and the vendor bills us
// for it either way.
if (!Object.hasOwn(ELEVENLABS_TTS_COSTS, modelId)) {
const expected = Object.keys(ELEVENLABS_TTS_COSTS);
throw new HttpError(
400,
`Invalid model: ${modelId}. Expected: ${expected.join(', ')}`,
{
legacyCode: 'field_invalid',
fields: { key: 'model', expected, got: modelId },
},
);
}
const desiredFormat =
output_format || response_format || DEFAULT_OUTPUT_FORMAT;
const actor = Context.get('actor')!;
const usageKey = `elevenlabs:${modelId}:character`;
const ucentsPerChar = ELEVENLABS_TTS_COSTS[modelId];
const totalCost = ucentsPerChar * text.length;
const usageAllowed = await this.meteringService.hasEnoughCredits(View on GitHub (pinned to 908ec23eda)
Solutions
- Use a model present in the cost table: eleven_multilingual_v2, eleven_flash_v2_5, eleven_turbo_v2_5, eleven_v3 (read fields.expected from the error).
- Omit model to accept the default (eleven_multilingual_v2).
- Call list_engines({ provider: 'elevenlabs' }) to see supported/priced model ids.
- If you need a new model priced, add it to ELEVENLABS_TTS_COSTS in costs.js.
Example fix
// before
await driver.synthesize({ text: 'hi', provider: 'elevenlabs', model: 'eleven_monolingual_v1' });
// after
await driver.synthesize({ text: 'hi', provider: 'elevenlabs', model: 'eleven_multilingual_v2' }); Defensive patterns
Strategy: validation
Validate before calling
// Gate on the priced model set, mirroring ELEVENLABS_TTS_COSTS keys.
const PRICED_MODELS = ['eleven_multilingual_v2', 'eleven_flash_v2_5', 'eleven_turbo_v2_5', 'eleven_v3'];
function synthesizeElevenLabs(text, model = 'eleven_multilingual_v2') {
if (!PRICED_MODELS.includes(model)) {
throw new Error(`Unsupported model: ${model}. Use one of: ${PRICED_MODELS.join(', ')}`);
}
return driver.synthesize({ text, provider: 'elevenlabs', model });
} Type guard
const PRICED_MODELS = ['eleven_multilingual_v2', 'eleven_flash_v2_5', 'eleven_turbo_v2_5', 'eleven_v3'] as const; type ElevenLabsModel = typeof PRICED_MODELS[number]; const isElevenLabsModel = (v: unknown): v is ElevenLabsModel => typeof v === 'string' && (PRICED_MODELS as readonly string[]).includes(v);
Prevention
- Let users pick models from list_engines({ provider: 'elevenlabs' }) output, not free text.
- When adding a model server-side, update ELEVENLABS_TTS_COSTS at the same time.
- Omit model to accept the safe default (eleven_multilingual_v2).
When it happens
Trigger: Passing model: 'eleven_monolingual_v1' (not in the cost table) or any model id absent from ELEVENLABS_TTS_COSTS, including deprecated or preview ids that ElevenLabs accepts but Puter cannot price.
Common situations: Using an old/deprecated model id; passing a brand-new ElevenLabs model before the cost table is updated; typo in the model id; assuming a model is supported because ElevenLabs lists it, when Puter's cost table hasn't added it.
Related errors
AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12).
Data as JSON: /api/errors/a25f89c8a622cf93.
Report an issue: GitHub.