{"record":{"id":"1c9e76a4dc622368","repo":"odysseus-dev/odysseus","slug":"not-an-object","errorCode":null,"errorMessage":"not an object","messagePattern":"not an object","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"warning","filePath":"routes/task_routes.py","lineNumber":1152,"sourceCode":"                url, model, headers = resolve_endpoint(\"default\", owner=user or None)\n            if not (url and model):\n                return {\"success\": False, \"message\": \"No model endpoint configured\"}\n            raw = await llm_call_async(\n                url=url, model=model,\n                messages=[{\"role\": \"system\", \"content\": sys},\n                          {\"role\": \"user\", \"content\": desc[:1000]}],\n                temperature=0.2, max_tokens=400, headers=headers, timeout=45,\n            )\n            text = _strip_think(raw or \"\", prose=False, prompt_echo=False).strip()\n            if text.startswith(\"```\"):\n                text = text.strip(\"`\")\n                if text.lower().startswith(\"json\"):\n                    text = text[4:].lstrip()\n            # Pull the first {...} block in case the model added stray text.\n            m = _re.search(r\"\\{.*\\}\", text, _re.S)\n            draft = _json.loads(m.group(0) if m else text)\n            if not isinstance(draft, dict):\n                raise ValueError(\"not an object\")\n            # Whitelist + light validation so the frontend gets clean fields.\n            out: Dict[str, Any] = {}\n            if draft.get(\"task_type\") in (\"llm\", \"research\"):\n                out[\"task_type\"] = draft[\"task_type\"]\n            else:\n                out[\"task_type\"] = \"llm\"\n            for k in (\"name\", \"prompt\", \"cron_expression\", \"scheduled_date\"):\n                if isinstance(draft.get(k), str) and draft[k].strip():\n                    out[k] = draft[k].strip()\n            if draft.get(\"schedule\") in (\"daily\", \"weekly\", \"monthly\", \"once\", \"cron\"):\n                out[\"schedule\"] = draft[\"schedule\"]\n            else:\n                out[\"schedule\"] = \"daily\"\n            st = draft.get(\"scheduled_time\")\n            if isinstance(st, str) and _re.match(r\"^\\d{1,2}:\\d{2}$\", st.strip()):\n                out[\"scheduled_time\"] = st.strip()\n            if isinstance(draft.get(\"scheduled_day\"), int):\n                out[\"scheduled_day\"] = draft[\"scheduled_day\"]","sourceCodeStart":1134,"sourceCodeEnd":1170,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L1134-L1170","documentation":"ValueError('not an object') raised inside POST /api/tasks/parse when the LLM's reply parses as valid JSON but is not a top-level object (e.g. a bare array or string). The handler strips markdown fences, extracts the first {...} block, json.loads it, then requires isinstance(draft, dict). The surrounding try/except converts it to a 200 response with success:false and message 'not an object' — a model-output quality failure, not a server fault.","triggerScenarios":"POST /api/tasks/parse with a description for which the model emits a JSON array or bare scalar instead of an object; or a reply whose first {...} extraction yields a non-dict value.","commonSituations":"Utility/default model endpoint misconfigured or pointed at a model that rambles; max_tokens=400 truncating the JSON so the {...} regex grabs a fragment; prompt-injection-style input that makes the model answer with a list of steps.","solutions":["Retry the request — non-dict outputs are typically non-deterministic at temperature 0.2","Switch the 'utility' (or fallback 'default') endpoint to a stronger instruction-following model via resolve_endpoint configuration","Shorten/simplify the description input so the model stays within max_tokens=400","Inspect the logged output (logger.error('parse_task failed: ...')) to see what shape the model actually returned"],"exampleFix":"// before: model returns [\"step1\",\"step2\"] -> success:false\n// after: strengthen the system prompt and budget\n\"Output ONLY a single JSON object enclosed in { }. Never an array, string, or multiple objects.\")\nllm_call_async(..., max_tokens=800, ...)","handlingStrategy":"retry","validationCode":"// Pre-validate the description before calling /parse\nif (!description || description.trim().length < 3) return skipParse();","typeGuard":"function isTaskDraft(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"const r = await parseTask(desc);\nif (!r.success) { if (r.message === 'not an object') return retryOnce(desc); else return manualForm(); }","preventionTips":["Retry once — LLM shape errors are transient at temperature 0.2","Always branch on response.success, not HTTP status (this route returns 200 with success:false)","Keep descriptions short so the model stays within max_tokens"],"tags":["llm","json","parsing","tasks","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}