Summary
clean_json_response(response) in src/memos/mem_os/utils/format_utils.py:1393 calls response.replace(...) unconditionally. When response is None it dies with:
AttributeError: 'NoneType' object has no attribute 'replace'
The traceback points to format_utils.py:1403 and gives no hint about the real problem (an upstream LLM call returning None).
How None reaches here
In the codebase today this is reachable via the suggestion endpoint:
POST /product/suggestions
→ suggestion_handler.handle_get_suggestion_queries
→ llm.generate(message_list) # OpenAILLM.generate, decorated with @timed_with_status
→ (LLM call raises BadRequestError)
→ timed_with_status catches, no fallback configured, falls through to implicit `return None`
→ clean_json_response(None)
→ AttributeError ← user sees this
The deeper bug is in timed_with_status (filed separately as #1523). Even after that is fixed, however, defending against None here is cheap and turns the worst possible diagnostic experience (a wrong-line AttributeError) into a clear message that names the actual root cause.
Proposed fix
if response is None:
raise ValueError(
"clean_json_response received None — upstream LLM call likely "
"failed silently (check timed_with_status / generate() error handling)."
)
return response.replace("```json", "").replace("```", "").strip()
PR follows.
Summary
clean_json_response(response)insrc/memos/mem_os/utils/format_utils.py:1393callsresponse.replace(...)unconditionally. WhenresponseisNoneit dies with:The traceback points to
format_utils.py:1403and gives no hint about the real problem (an upstream LLM call returningNone).How
Nonereaches hereIn the codebase today this is reachable via the suggestion endpoint:
The deeper bug is in
timed_with_status(filed separately as #1523). Even after that is fixed, however, defending againstNonehere is cheap and turns the worst possible diagnostic experience (a wrong-line AttributeError) into a clear message that names the actual root cause.Proposed fix
PR follows.