{"record":{"id":"fb98984f4b755f80","repo":"jamiepine/voicebox","slug":"invalid-llm-size-model-size-must-be-one-of","errorCode":null,"errorMessage":"Invalid LLM size '{model_size}'. Must be one of: {sorted(valid_sizes)}","messagePattern":"Invalid LLM size '(.+?)'\\. Must be one of: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/routes/llm.py","lineNumber":27,"sourceCode":"from ..backends import get_llm_model_configs\nfrom ..services import llm\nfrom ..services.task_queue import create_background_task\nfrom ..utils.tasks import get_task_manager\n\nlogger = logging.getLogger(__name__)\n\nrouter = APIRouter()\n\n\n@router.post(\"/llm/generate\", response_model=models.LLMGenerateResponse)\nasync def llm_generate(request: models.LLMGenerateRequest):\n    \"\"\"Run a single-turn Qwen3 completion.\"\"\"\n    backend = llm.get_llm_model()\n    model_size = request.model_size or backend.model_size\n\n    valid_sizes = {cfg.model_size for cfg in get_llm_model_configs()}\n    if model_size not in valid_sizes:\n        raise HTTPException(\n            status_code=400,\n            detail=f\"Invalid LLM size '{model_size}'. Must be one of: {sorted(valid_sizes)}\",\n        )\n\n    already_loaded = backend.is_loaded() and backend.model_size == model_size\n    if not already_loaded and not backend._is_model_cached(model_size):\n        progress_model_name = f\"qwen3-{model_size.lower()}\"\n        task_manager = get_task_manager()\n\n        async def download_llm_background():\n            try:\n                await backend.load_model(model_size)\n                task_manager.complete_download(progress_model_name)\n            except Exception as e:\n                task_manager.error_download(progress_model_name, str(e))\n\n        task_manager.start_download(progress_model_name)\n        create_background_task(download_llm_background())","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/llm.py#L9-L45","documentation":"Returned by POST /llm/generate when the resolved model_size is not one of the registered Qwen3 LLM sizes. valid_sizes is built at runtime from get_llm_model_configs(), which yields exactly {'0.6B','1.7B','4B'}. model_size comes from request.model_size (defaulting via the Pydantic field) or falls back to backend.model_size, so a drifted backend default can also trip it even when the client omits the field.","triggerScenarios":"POST /llm/generate with body {\"prompt\":\"...\",\"model_size\":\"7B\"} (or \"2B\", \"8b\", \"0.6b\" with wrong case, etc.); also reachable if request.model_size is omitted/null and the live LLM backend's model_size attribute has been set to a value outside the registry (e.g. monkey-patched, or a config refactor that changed size strings).","commonSituations":"Client hard-coded an outdated size after an upgrade that renamed sizes; frontend dropdown populated from a stale list; casing mismatch ('0.6b' vs '0.6B'); backend default drift after manually reassigning backend.model_size for testing.","solutions":["Send one of the documented sizes: \"0.6B\", \"1.7B\", or \"4B\" (exact case, capital B).","Omit model_size entirely to use the backend's configured default for that request.","If you maintain the client, fetch the valid set dynamically rather than hard-coding — call GET /models/status or read the LLMGenerateRequest schema and filter to qwen_llm engine configs.","If you hit this while omitting model_size, check that the LLM backend instance has not had its model_size reassigned to a non-registry value at startup."],"exampleFix":"// before\nfetch('/llm/generate', {method:'POST', body: JSON.stringify({prompt: text, model_size: '7B'})})\n// after\nfetch('/llm/generate', {method:'POST', body: JSON.stringify({prompt: text, model_size: '4B'})})","handlingStrategy":"validation","validationCode":"const VALID_LLM_SIZES = ['0.6B','1.7B','4B'];\nfunction pickSize(req) {\n  const size = req.model_size || '0.6B';\n  if (!VALID_LLM_SIZES.includes(size)) {\n    throw new Error(`model_size must be one of ${VALID_LLM_SIZES.join(', ')}`);\n  }\n  return size;\n}\n// before fetch:\nconst model_size = pickSize(body);\nfetch('/llm/generate', {method:'POST', body: JSON.stringify({...body, model_size})});","typeGuard":"function isLlmSize(x: unknown): x is '0.6B' | '1.7B' | '4B' {\n  return typeof x === 'string' && ['0.6B','1.7B','4B'].includes(x);\n}","tryCatchPattern":"try {\n  const res = await fetch('/llm/generate', {method:'POST', body: JSON.stringify(payload)});\n  if (res.status === 400) {\n    const err = await res.json();\n    // err.detail lists valid sizes — refresh client's size list and surface to user\n    throw new UserInputError(err.detail);\n  }\n  return await res.json();\n} catch (e) { /* network/retry logic */ }","preventionTips":["Derive the size list from GET /models/status filtered to engine==='qwen_llm' instead of hard-coding.","Populate the size dropdown from the live registry so stale ids are impossible.","Omit model_size when you want the backend default rather than sending a guess."],"tags":["llm","validation","fastapi","qwen","http-400"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}