Skip to content

fix: fallback content to reasoning_content when DeepSeek returns empty content field#428

Merged
pancacake merged 8 commits into
HKUDS:devfrom
Starfie1d1272:fix/deepseek-reasoning-content-fallback
May 4, 2026
Merged

fix: fallback content to reasoning_content when DeepSeek returns empty content field#428
pancacake merged 8 commits into
HKUDS:devfrom
Starfie1d1272:fix/deepseek-reasoning-content-fallback

Conversation

@Starfie1d1272

@Starfie1d1272 Starfie1d1272 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Problem

DeepSeek v4-flash and v4-pro default to thinking mode enabled (per API docs), returning responses in reasoning_content while leaving content empty. This caused cascade failures across multiple layers:

Symptom Root cause
LLM returned empty response _parse() / _parse_chunks() only fell back to m.reasoning (DashScope), not m.reasoning_content (DeepSeek)
No JSON object found in book blocks CodeGenerator / FlashCardsGenerator / TimelineGenerator / DeepDiveGenerator used fragile json.loads() directly
Streaming empty response factory.py wrapped reasoning in <think> tags stripped by clean_thinking_tags()
'list' object has no attribute 'get' IdeaAgent didn't guard against array-type JSON responses
LLM_REASONING_EFFORT env var silently ignored resolver path takes priority over .env; env override not applied

Fix

1. Content fallback + precedence alignment

  • _parse() / _parse_chunks() in both provider_core and tutorbot: fall back reasoning_contentcontent, check reasoning_content before reasoning

2. Streaming path

  • factory.py _runner: when only reasoning chunks are emitted (no _on_content_delta), emit response.content as fallback

3. Book block JSON parsing

  • code.py, flash_cards.py, timeline.py, deep_dive.py: replace raw json.loads()parse_json_response() which handles markdown fences, preamble, and json_repair fallback

4. LLM_REASONING_EFFORT env var

  • config.py: read from env and apply as override regardless of resolver path
  • factory.py: pass config.reasoning_effort to chat_with_retry / chat_stream_with_retry
  • For DeepSeek: high/max enables thinking, minimal sends thinking: {type: "disabled"}, empty → auto-detect
  • .env.example / .env.example_CN: documented

5. idea_agent robustness

  • Guard against JSON array and non-dict responses

Scope note

This PR is a bug fix — it prevents crashes and empty responses when thinking mode is active. Full thinking mode support (leveraging reasoning_content to improve generation quality, token budget allocation, animation pipeline adaptation) is tracked in #430.

Also tracked in #430: book generation UX improvements (force regenerate, failure diagnostics, prompt leakage prevention, i18n).

Test plan

  • deepseek-v4-flash + LLM_REASONING_EFFORT=minimal: all book blocks generate without errors
  • deepseek-v4-flash + LLM_REASONING_EFFORT=high: reasoning_content falls back correctly, no empty responses
  • deepseek-chat / gpt-4o regression: normal operation unchanged
  • Idea generation handles malformed LLM JSON

…y content field

DeepSeek models (v4-flash, reasoner, etc.) return the actual response in the
reasoning_content field while leaving content empty when thinking mode is
enabled. The _parse() method only fell back to m.reasoning (DashScope-style),
causing "LLM returned empty response" errors.

Also fix idea_agent to handle LLM responses that are JSON arrays instead of
objects with an "ideas" key.
Copilot AI review requested due to automatic review settings April 30, 2026 18:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes DeepSeek “thinking mode” responses and improves idea generation robustness by handling alternate response shapes across OpenAI-compatible providers and agents.

Changes:

  • Update OpenAI-compat _parse() (services provider_core + tutorbot) to fall back to reasoning_content when content is empty.
  • Update IdeaAgent to accept JSON array payloads by normalizing them into an {"ideas": ...} object.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
deeptutor/tutorbot/providers/openai_compat_provider.py Adds reasoning_contentcontent fallback during response parsing.
deeptutor/services/llm/provider_core/openai_compat_provider.py Adds the same reasoning_contentcontent fallback in the services-layer provider.
deeptutor/agents/question/agents/idea_agent.py Wraps array-style JSON responses into an object with an "ideas" key to prevent crashes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread deeptutor/agents/question/agents/idea_agent.py
Comment thread deeptutor/tutorbot/providers/openai_compat_provider.py Outdated
Comment thread deeptutor/services/llm/provider_core/openai_compat_provider.py Outdated
…dence

- Swap reasoning_content/reasoning fallback order in _parse() to match
  the precedence used when extracting reasoning_content (per Copilot review)
- Add reasoning_content → content fallback in _parse_chunks() for both
  provider_core and tutorbot providers
- Fix streaming path in factory.py: when only reasoning chunks are
  emitted (no direct content), fall back to response.content so
  downstream consumers receive non-empty responses
- Harden idea_agent against non-dict JSON payloads
- Read LLM_REASONING_EFFORT from environment in config.py
- Pass config.reasoning_effort to chat_with_retry/chat_stream_with_retry
  in both complete() and stream() (was previously dropped)
- Set to "low"/"medium"/"high" to enable thinking, leave empty to use
  automatic detection based on reasoning_model_patterns
DeepSeek only supports high/max for reasoning_effort; minimal is a
DeepTutor convention that maps to thinking.type=disabled. Update comments
to be accurate for each provider.
Replace fragile json.loads() with parse_json_response() in code,
flash_cards, timeline, and deep_dive generators. Handles LLM
responses with markdown fences, preamble text, and malformed JSON.
get_llm_config() prefers the resolver path over .env, so the env var
was silently ignored. Apply it as an override regardless of which path
produced the config.
…-content-fallback

# Conflicts:
#	deeptutor/book/blocks/code.py
#	deeptutor/book/blocks/deep_dive.py
#	deeptutor/book/blocks/flash_cards.py
#	deeptutor/book/blocks/timeline.py
@pancacake pancacake merged commit b1faa72 into HKUDS:dev May 4, 2026
@pancacake

Copy link
Copy Markdown
Collaborator

Thanks for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants