Skip to content

fix(core): fail model empty-completion before retry - #2968

Open
Buktal wants to merge 1 commit into
agentscope-ai:mainfrom
Buktal:fix/2962-empty-completion-silent-success
Open

fix(core): fail model empty-completion before retry#2968
Buktal wants to merge 1 commit into
agentscope-ai:mainfrom
Buktal:fix/2962-empty-completion-silent-success

Conversation

@Buktal

@Buktal Buktal commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

AgentScope-Java Version

main @ ea511ec2 (defect observed in production on 2.0.1)

Description

Fixes #2962

When a model provider (or an LLM gateway in front of it) returns HTTP 200 with a stream that completes without any content-bearing chunk — e.g. only the [DONE] marker, or only well-formed chunks whose content parses to empty — the entire chain treated it as a normal successful completion: no error, no retry, no fallback, and no assistant message persisted. To the caller it looked like the model simply chose to say nothing.

Changes

ModelUtils.applyTimeoutAndRetry — the shared path all model implementations route through — now detects empty completions and converts them to a ModelException before retryWhen:

  • Detection: ensureNonEmptyCompletion (new private helper) rejects a stream that completes without any chunk carrying a non-null, non-empty getContent(). Any content block counts (multimodal blocks included); only a fully contentless stream is rejected.
  • Withholding: contentless chunks are withheld until the first content-bearing chunk arrives, then released in order. This guarantees the error is the first downstream signal on an empty completion: switchOnFirst-based fallback commits on the first signal, and the common OpenAI-compatible empty shape (a role-only first chunk, then [DONE]) would otherwise emit a contentless chunk first and bypass the fallback model entirely. Healthy streams are unaffected — withheld chunks are released the moment content arrives, with order and metadata preserved.
  • Placement: after timeout (a timed-out request goes straight to error) and before retry (an empty completion triggers retry like any other model error).
  • Unconditional: the check applies regardless of retry configuration, so a bare agent.call(...) without an ExecutionConfig still surfaces the error instead of ending as silent success.
  • Hardening: applyTimeoutAndRetry no longer NPEs when both options and defaultOptions are null — GenerateOptions.mergeOptions documents that it can return null, and both parameters are documented as nullable.

Tests

ModelTimeoutRetryTest — 6 tests:

  1. Zero chunks (Flux.empty(), the [DONE]-only case) → error.
  2. Two contentless chunks → error as the first downstream signal (no chunk is emitted before it, so a switchOnFirst-based fallback still engages).
  3. Empty first attempt, success on retry → retry engages, attemptCount == 2.
  4. Content-then-mid-stream-error followed by an empty retry attempt → still fails (RetryExhaustedException wrapping ModelException).
  5. Image-only (multimodal) chunks → completes normally.
  6. Leading contentless chunks are released in order once content arrives; trailing contentless chunks pass through.

ReActAgentTest: primary model returns an empty completion (contentless chunks, then complete) → fallback model takes over and answers.

Provider extension tests: mocks that stubbed contentless responses (choices: [], or streaming bodies without any content-bearing delta) were updated to return realistic content-bearing responses, since a contentless completion is now an error by design (OpenAIChatModelTest, DashScopeChatModelTest, DashScopeNonStreamingBlockingBehaviorTest).

Checklist

  • Code has been formatted with mvn spotless:apply
  • All tests are passing (mvn test -pl agentscope-core, plus the full agentscope-extensions-model reactor)
  • Javadoc comments are complete and follow project conventions
  • Related documentation has been updated (n/a — internal behavior change, covered by Javadoc)
  • Code is ready for review

ModelUtils.applyTimeoutAndRetry's shared path (all model implementations)
now detects empty completions—streams with zero chunks or only chunks
whose content blocks are not TextBlock/ThinkingBlock/ToolUseBlock—and
converts them to ModelException before retryWhen, so the existing retry
and fallback chains engage.

Detection is unconditional (applies regardless of ExecutionConfig/retry
configuration), since empty completion is an upstream transport anomaly
that should always surface as an error. Detection sits after timeout
(a timed-out request bypasses the check and goes straight to error).

Three regression tests: zero-chunk stream, all-empty-content chunks,
and retry-then-success (confirming the error triggers retry and a
subsequent non-empty response completes successfully).

Fixes agentscope-ai#2962
@Buktal
Buktal force-pushed the fix/2962-empty-completion-silent-success branch from aa64284 to 3839e8b Compare September 3, 2026 13:31
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@jujn jujn self-assigned this Sep 6, 2026
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.

[Bug]: Zero-chunk model completion ends the ReAct run as silent success - no error, no retry, no fallback

2 participants