Skip to content

[Bug]: DeepSeek PR review fails after three reasoning-only responses #429

Closed

Description

@Sun-sunshine06

Summary

The Codex PR Review workflow is routed to the DeepSeek review script for the current provider configuration. On PR #428, the configured deepseek-v4-flash model returned an empty final message.content on all three attempts, while returning substantial reasoning_content. The script failed before posting any GitHub review.

This is a review-model/output integration failure, not a code-review rejection or a failed product test. The deeper cause (provider behavior, thinking configuration, or output-budget exhaustion) is not yet proven by the retained logs.

Evidence

Sanitized failure shape:

DeepSeek output invalid on attempt 1/3: DeepSeek API returned no message content
DeepSeek output invalid on attempt 2/3: DeepSeek API returned no message content
Error: DeepSeek API returned no message content
Process completed with exit code 1

The response previews show message.content: "" with non-empty reasoning_content. The workflow did not run the Codex action fallback and did not submit any review. Request credentials/endpoints are intentionally omitted here.

Current implementation and suspected contributors

  • Review entrypoint does not override the output budget.
  • Shared request helper defaults to max_tokens: 8192, requests JSON output, and only explicitly enables thinking for deepseek-reasoner / deepseek-v4-pro*. For deepseek-v4-flash it omits the thinking setting; the observed response nevertheless contains reasoning.
  • Retry handling adds a JSON-format reminder but keeps the same budget/model/thinking policy.
  • Empty-content diagnostics truncate the raw response before the useful trailing finish_reason / usage fields, so the logs do not establish whether the output budget was exhausted.

A large review input (27 changed files, 3938 additions in this case) plus reasoning consuming the output budget is a plausible hypothesis, not a confirmed root cause.

Expected behavior

A valid, non-empty JSON review body is posted, or the workflow clearly reports that automated review was unavailable. Model/provider failures must not be presented as code findings, and reasoning text must not be substituted for a completed review or approval.

Suggested follow-up / acceptance criteria

  • Log safe structured metadata on empty output: requested/returned model, finish reason, usage, and content/reasoning lengths; do not dump credentials, prompts, or raw reasoning.
  • Verify the provider's current thinking/output-budget semantics for deepseek-v4-flash and configure them explicitly where supported.
  • Add a bounded, cost-aware recovery policy for reasoning-only or length-exhausted output instead of repeating the same ineffective request three times (appropriate output budget, smaller review chunks, or an explicitly configured fallback).
  • Add mocked API regression tests for reasoning-only output, finish_reason=length, valid final JSON, malformed JSON, and exhausted retries; assert no blank/fabricated review is posted.
  • Reproduce with a similarly sized PR and verify successful review submission or an actionable infrastructure-failure summary.

Merge disposition

Per the maintainer's explicit request, #428 was squash-merged after verifying that CI (lint/typecheck/tests/build), CodeQL, and Dependency Review all passed, with no submitted blocking review. The only failing check was this automated review execution. The merge used an exact head-SHA guard and did not use an admin override or change repository protections.

Merge commit: c25a5765c6ce7eec6c7e184643613bfcd3bfa403.

Activity

  1. github-actions commented on Sep 19, 2026

    @github-actions
    Contributor

    Thanks for the detailed report — the failure shape, the retained evidence, and the separation between "automated review unavailable" and "code-review rejection" are all clear and useful. This is a defect in the repo's own CI review tooling under .github/scripts/, not in the shipped app, so it doesn't affect the design pipeline; still worth fixing so the review gate produces an honest result instead of an opaque red check.

    What I can confirm from the public checkout

    • .github/scripts/deepseek-pr-review.mjs:3 — the PR review entrypoint imports callDeepSeekJsonWithRetries from the shared helper, so the request shape, JSON-mode handling, and retry policy all live in one place.
    • .github/scripts/deepseek-issue-response.mjs:3 and :19 — the issue responder uses the same helper and the same "you are running through DeepSeek chat completions" framing. Any fix to the shared helper covers both workflows, which is the right place to land it.
    • .github/scripts/deepseek-common.mjs:205-206 — the helper normalizes base URLs by stripping /v1/responses / /responses suffixes, i.e. it targets chat-completions-style endpoints. That is consistent with the request shape you describe.

    The specific line ranges you cite (deepseek-pr-review.mjs L277–L284, deepseek-common.mjs L224–L298 and L341–L375) are not verifiable from the material available to me, so I'm treating the "no output-budget override" and "thinking enabled only for deepseek-reasoner / deepseek-v4-pro*" claims as your reading of the current head rather than as confirmed by me. They're plausible and match the observed behavior, but the exact offsets may have moved.

    On the mechanism

    The error string in your log (DeepSeek API returned no message content) shows the script already has an explicit non-empty-content guard, and that guard is why nothing was posted. That part is working as intended — the problem is the recovery policy, not the validation.

    Two things in your evidence stand out as worth pinning down before calling a root cause:

    1. Thinking appears to be on by default for this model. The helper omits the thinking setting for deepseek-v4-flash yet the response still carries non-empty reasoning_content. If reasoning tokens are counted against max_tokens on this endpoint, then a 27-file / 3938-addition diff plus reasoning can plausibly consume the entire budget and leave content empty. That's a hypothesis your logs can't currently confirm or refute — which is exactly why your first checklist item (log finish_reason, usage, and content/reasoning lengths without dumping the prompt) is the right next step. finish_reason: "length" would settle it immediately.
    2. Model field mismatch. You configured deepseek-v4-flash but the response reports deepseek-flash. That may be a harmless alias/normalization on the provider side, or it may indicate the request is being routed to a different model family than the one whose thinking semantics you're assuming. Worth confirming what the provider echoes back versus what you send.

    Your acceptance criteria read correctly to me: bounded recovery (raise the budget or chunk the diff) instead of three identical retries, mocked regression tests for reasoning-only output / finish_reason=length / valid JSON / malformed JSON / exhausted retries, and an assertion that no blank or fabricated review is ever posted. The last one matters most — an automatic review that silently substitutes reasoning text for findings would be worse than this failure.

    Need more info (if not already retained)

    • The sanitized response envelope from one failed attempt: finish_reason, usage (completion / reasoning token counts if exposed), and the returned model string.
    • The redacted base-URL shape, wire type, and any provider documentation on whether reasoning tokens count against max_tokens for this model.
    • Whether the empty-content result reproduces with a small (1–2 file) diff or with the output budget raised. That single experiment separates "budget exhausted by large input + reasoning" from "endpoint returns reasoning-only regardless of budget," which determines whether the fix is chunking or a fallback path.
    • If a related report or PR already covers this same no message content signature, link it and I'll fold it in rather than track it twice.

    Open-CoDesign Bot

  2. Sun-sunshine06 commented on Oct 13, 2026

    @Sun-sunshine06
    CollaboratorAuthor

    Closing as completed after verifying live behavior with the repair from #431 in the trusted base.

    Acceptance is for the verified current deepseek-flash/high configuration. The original incident used deepseek-v4-flash/high; its retained logs lack finish reason/usage, so output-budget exhaustion remains an unproven sole cause of that historical incident. The later length-exhaustion case and successful recovery are directly verified.

    These results establish automation availability and actual review submission, not code approval: the posted reviews are COMMENTED. Broader context coverage, batching, and live validation of the shared issue-response caller are outside this closure claim. No new provider calls or blind reruns were needed for this verification.

  3. github-actions commented on Oct 13, 2026

    @github-actions
    Contributor

    Thanks for the detailed closure and for separating what the live runs actually verified from what remains unproven. That distinction is the useful part of this report — the original deepseek-v4-flash incident still has no retained finish_reason/usage, so output-budget exhaustion is a plausible but unestablished cause for that run, while the later finish_reason=length case plus successful recovery after the budget increase is directly evidenced. Recording both without collapsing them into a single root cause is the right call.

    I can't independently re-verify the linked workflow runs, job logs, or posted reviews from the public checkout, so I'm taking those as your verification rather than restating them as confirmed here. Two points I can check against the repository itself:

    • The shared helper is the right single place for this repair, since both bot callers go through the same request/retry path — a budget or recovery-policy change there covers the PR-review workflow and the issue-response caller together.
    • The mocked-test harness the closure cites is the repo's own: package.json — "test:scripts": "node --test .github/scripts/deepseek-common.test.mjs". Any regression suite for reasoning-only output, finish_reason=length, valid/malformed JSON, and exhausted retries belongs in that file so it runs on the normal pnpm test path.

    Your framing of the outcome is consistent with how automated review should be treated: successful step execution and an actual submitted review establish that the automation is available, not that the code is approved — the reviews are COMMENTED, and that is not a substitute for human or blocking review.

    The items you explicitly left outside the closure — broader context coverage, diff batching, and live validation of the shared issue-response caller — are worth keeping visible as follow-ups rather than folding into a completed bug. If you open dedicated issues for those, I can triage them on their own terms rather than reopening this one.


    Open-CoDesign Bot

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions