AI classification fails when model returns empty content (JSONDecodeError) #106

Closed
opened 2026-09-02 08:46:47 +00:00 by agent-pi · 1 comment
Collaborator

Symptom

Intermittent AI classification failures where the model's HTTP request returns 200 OK but json.loads() raises JSONDecodeError: Expecting value: line 1 column 1 (char 0) — i.e. the message.content field is empty.

Affected posts (from packages/fenliu/fenliu-all.log):

  • Vision stage: 15, 22
  • Text stage: 41

Root cause

AIClient.complete_json does:

return json.loads(payload["choices"][0]["message"]["content"])

For the failing posts the model returned 200 OK but message.content was empty (char 0 = empty input), so json.loads("") raised. The exception is caught in classify_posts, which logs a warning and continues, leaving the post unclassified.

Traceback from logs/fenliu_debug.log:

  File ".../services/ai_classification.py", line 161, in complete_json
    return json.loads(payload["choices"][0]["message"]["content"])
  File ".../json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Possible causes (not confirmed)

The request sends "thinking": {"type": "disabled"}, "response_format": {"type": "json_object"}, and max_tokens=300 against Moonshot kimi-k2.6. Candidates to investigate:

  1. The model emits reasoning into reasoning_content and leaves content empty (thinking toggle not honored).
  2. A content-moderation/filter response returns empty content.
  3. max_tokens=300 truncates before any content is produced.

The raw response body is not logged, so the exact cause can't be pinned down from current logs.

Fix direction

Make complete_json robust and observable:

  • Guard json.loads — on empty/unparseable content, fail closed (return an "unclassified" signal rather than raising).
  • Log the raw payload (at least finish_reason, content, reasoning_content) at DEBUG when parsing fails, so the cause is visible next time.
  • Consider whether thinking should be enabled for kimi-k2.6 and the answer read from reasoning_content, or whether empty-content responses should be retried.

Impact

Affected posts are left AI: pending (manual review). No mis-classification — just unclassified posts.

## Symptom Intermittent AI classification failures where the model's HTTP request returns `200 OK` but `json.loads()` raises `JSONDecodeError: Expecting value: line 1 column 1 (char 0)` — i.e. the `message.content` field is empty. Affected posts (from `packages/fenliu/fenliu-all.log`): - Vision stage: **15, 22** - Text stage: **41** ## Root cause `AIClient.complete_json` does: ```python return json.loads(payload["choices"][0]["message"]["content"]) ``` For the failing posts the model returned `200 OK` but `message.content` was empty (char 0 = empty input), so `json.loads("")` raised. The exception is caught in `classify_posts`, which logs a warning and `continue`s, leaving the post unclassified. Traceback from `logs/fenliu_debug.log`: ``` File ".../services/ai_classification.py", line 161, in complete_json return json.loads(payload["choices"][0]["message"]["content"]) File ".../json/decoder.py", line 363, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) ``` ## Possible causes (not confirmed) The request sends `"thinking": {"type": "disabled"}`, `"response_format": {"type": "json_object"}`, and `max_tokens=300` against Moonshot `kimi-k2.6`. Candidates to investigate: 1. The model emits reasoning into `reasoning_content` and leaves `content` empty (thinking toggle not honored). 2. A content-moderation/filter response returns empty content. 3. `max_tokens=300` truncates before any content is produced. The raw response body is not logged, so the exact cause can't be pinned down from current logs. ## Fix direction Make `complete_json` robust and observable: - Guard `json.loads` — on empty/unparseable content, fail closed (return an "unclassified" signal rather than raising). - Log the raw payload (at least `finish_reason`, `content`, `reasoning_content`) at DEBUG when parsing fails, so the cause is visible next time. - Consider whether `thinking` should be enabled for kimi-k2.6 and the answer read from `reasoning_content`, or whether empty-content responses should be retried. ## Impact Affected posts are left `AI: pending` (manual review). No mis-classification — just unclassified posts.
Author
Collaborator

Plan of attack:

  1. packages/fenliu/src/fenliu/services/ai_classification.pycomplete_json guards the whole choices[0].message.content extraction and parse; empty/missing/non-JSON content raises a new typed AIMalformedResponseError. Before raising it logs a WARNING with finish_reason, the raw content repr, and a bounded reasoning_content excerpt (~500 chars), so the candidate causes (thinking toggle ignored / content filter / max_tokens truncation) are distinguishable on the next occurrence.
  2. packages/fenliu/src/fenliu/services/ai_pipeline.py — catches the typed exception before the generic except Exception and logs a terse one-liner with the post id, no traceback. The post stays AI: pending for manual review (deliberate: no retry, no fail-closed verdict — retry decision deferred until the logs reveal the actual cause).
  3. Tests (red first): the three malformed shapes raise the typed error, and classify_posts leaves the post unclassified when a stage hits empty content.
  4. Branch fix/issue-106-handle-empty-ai-content, single commit, PR to main.
Plan of attack: 1. `packages/fenliu/src/fenliu/services/ai_classification.py` — `complete_json` guards the whole `choices[0].message.content` extraction and parse; empty/missing/non-JSON content raises a new typed `AIMalformedResponseError`. Before raising it logs a WARNING with `finish_reason`, the raw `content` repr, and a bounded `reasoning_content` excerpt (~500 chars), so the candidate causes (thinking toggle ignored / content filter / `max_tokens` truncation) are distinguishable on the next occurrence. 2. `packages/fenliu/src/fenliu/services/ai_pipeline.py` — catches the typed exception before the generic `except Exception` and logs a terse one-liner with the post id, no traceback. The post stays `AI: pending` for manual review (deliberate: no retry, no fail-closed verdict — retry decision deferred until the logs reveal the actual cause). 3. Tests (red first): the three malformed shapes raise the typed error, and `classify_posts` leaves the post unclassified when a stage hits empty content. 4. Branch `fix/issue-106-handle-empty-ai-content`, single commit, PR to `main`.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/dujiangyan#106
No description provided.