odysseus-dev/odysseus · warning · ValueError

not an object

Error message

not an object

What it means

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.

Source

Thrown at routes/task_routes.py:1152

                url, model, headers = resolve_endpoint("default", owner=user or None)
            if not (url and model):
                return {"success": False, "message": "No model endpoint configured"}
            raw = await llm_call_async(
                url=url, model=model,
                messages=[{"role": "system", "content": sys},
                          {"role": "user", "content": desc[:1000]}],
                temperature=0.2, max_tokens=400, headers=headers, timeout=45,
            )
            text = _strip_think(raw or "", prose=False, prompt_echo=False).strip()
            if text.startswith("```"):
                text = text.strip("`")
                if text.lower().startswith("json"):
                    text = text[4:].lstrip()
            # Pull the first {...} block in case the model added stray text.
            m = _re.search(r"\{.*\}", text, _re.S)
            draft = _json.loads(m.group(0) if m else text)
            if not isinstance(draft, dict):
                raise ValueError("not an object")
            # Whitelist + light validation so the frontend gets clean fields.
            out: Dict[str, Any] = {}
            if draft.get("task_type") in ("llm", "research"):
                out["task_type"] = draft["task_type"]
            else:
                out["task_type"] = "llm"
            for k in ("name", "prompt", "cron_expression", "scheduled_date"):
                if isinstance(draft.get(k), str) and draft[k].strip():
                    out[k] = draft[k].strip()
            if draft.get("schedule") in ("daily", "weekly", "monthly", "once", "cron"):
                out["schedule"] = draft["schedule"]
            else:
                out["schedule"] = "daily"
            st = draft.get("scheduled_time")
            if isinstance(st, str) and _re.match(r"^\d{1,2}:\d{2}$", st.strip()):
                out["scheduled_time"] = st.strip()
            if isinstance(draft.get("scheduled_day"), int):
                out["scheduled_day"] = draft["scheduled_day"]

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Retry the request — non-dict outputs are typically non-deterministic at temperature 0.2
  2. Switch the 'utility' (or fallback 'default') endpoint to a stronger instruction-following model via resolve_endpoint configuration
  3. Shorten/simplify the description input so the model stays within max_tokens=400
  4. Inspect the logged output (logger.error('parse_task failed: ...')) to see what shape the model actually returned

Example fix

// before: model returns ["step1","step2"] -> success:false
// after: strengthen the system prompt and budget
"Output ONLY a single JSON object enclosed in { }. Never an array, string, or multiple objects.")
llm_call_async(..., max_tokens=800, ...)
Defensive patterns

Strategy: retry

Validate before calling

// Pre-validate the description before calling /parse
if (!description || description.trim().length < 3) return skipParse();

Type guard

function isTaskDraft(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

Try / catch

const r = await parseTask(desc);
if (!r.success) { if (r.message === 'not an object') return retryOnce(desc); else return manualForm(); }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/1c9e76a4dc622368. Report an issue: GitHub.