virattt/ai-hedge-fund · error · LLMParseError

no JSON object found in response: {text[:200]!r}

Error message

no JSON object found in response: {text[:200]!r}

What it means

Raised by extract_json (hedge_fund/llm/client.py:222) as LLMParseError (a ValueError) when the LLM response contains no parsable JSON object. The function tries three strategies — a ```json fenced block, the whole string, then the first balanced {...} sequence via brace counting — and only raises after all fail, including when a balanced block exists but json.loads rejects it.

Source

Thrown at hedge_fund/llm/client.py:222

        return json.loads(text.strip())
    except json.JSONDecodeError:
        pass

    start = text.find("{")
    if start != -1:
        depth = 0
        for i, ch in enumerate(text[start:], start):
            if ch == "{":
                depth += 1
            elif ch == "}":
                depth -= 1
                if depth == 0:
                    try:
                        return json.loads(text[start : i + 1])
                    except json.JSONDecodeError:
                        break

    raise LLMParseError(f"no JSON object found in response: {text[:200]!r}")

View on GitHub (pinned to eff8a7320f)

Solutions

  1. Retry the LLM call — transient non-JSON output often resolves on re-ask; the calling agent layer should catch LLMParseError and retry once or twice.
  2. Strengthen the prompt: demand 'Respond with ONLY a JSON object {"signal": ..., "confidence": ..., "reasoning": ...}' and, if the transport supports it, use a JSON/response-format mode.
  3. Raise max_tokens in make_llm so the JSON is never truncated.
  4. If the model returns arrays or nested junk, pre-normalize (take the first dict from a list) before extract_json.

Example fix

# before
sig = agent.generate_signal(ticker, snapshot)  # model replied in prose -> LLMParseError

# after
from hedge_fund.llm.client import LLMParseError

for attempt in range(3):
    try:
        sig = agent.generate_signal(ticker, snapshot)
        break
    except LLMParseError:
        if attempt == 2:
            raise
        # re-ask; optionally append 'Answer with JSON only.' to the prompt
Defensive patterns

Strategy: retry

Validate before calling

import json

def response_likely_has_json(text: str) -> bool:
    """Cheap pre-check mirroring extract_json's strategy order."""
    if "```" in text:
        return True
    if text.strip().startswith("{") and text.strip().endswith("}"):
        try:
            json.loads(text)
            return True
        except json.JSONDecodeError:
            pass
    return "{" in text and "}" in text

Type guard

from hedge_fund.llm.client import LLMParseError

def is_llm_parse_failure(e: BaseException) -> bool:
    return isinstance(e, LLMParseError)

Try / catch

from hedge_fund.llm.client import LLMParseError

for attempt in range(3):
    try:
        sig = agent.generate_signal(ticker, snapshot)
        return sig
    except LLMParseError:
        if attempt == 2:
            raise  # persistent: surface it, don't fabricate a neutral signal
        # optional: append 'Respond with ONLY a JSON object.' to the re-ask

Prevention

When it happens

Trigger: An LLM agent signal (LLMAgent._parse calls extract_json on the model's reply) where the model: answers in prose with no braces at all; emits single-quoted or trailing-comma JSON (json.loads fails inside the balanced-brace attempt, breaking out of the scan); wraps the object in a fence labeled something other than json; returns an empty string; prefixes a long chatty preamble that makes the first {...} close at the wrong place (depth returns to 0 early on inline braces).

Common situations: Weaker/cheaper models ignoring the JSON-format instruction; prompt changed to allow prose; max_tokens set so low the JSON is truncated mid-object; model returning a JSON array [...] instead of an object {...}.

Related errors


AI-assisted analysis of virattt/ai-hedge-fund@eff8a7320f (2026-08-15). Data as JSON: /api/errors/62dd721861b9ebc8. Report an issue: GitHub.