Skip to content

Empty stream (NoOutputGeneratedError) should be retried by RetryManager #575

@buger

Description

@buger

Problem

When the AI provider returns an empty stream (zero steps recorded), the Vercel AI SDK throws NoOutputGeneratedError with message "No output generated. Check the stream for errors.". This error is not retried by ProbeAgent, causing the entire check to fail on what is usually a transient provider hiccup.

Production example: task 70d0b650 — a simple "hi!" greeting to gemini-3.1-pro-preview resulted in an empty stream. The user saw:

"The AI provider rate limit was reached before a response could be generated."

Root Cause

The retry boundary is in the wrong place:

  1. RetryManager.executeWithRetry() wraps the streamText() call (ProbeAgent.js:4429)
  2. streamText() returns immediately — it doesn't throw
  3. The actual error surfaces later when await result.steps is called (ProbeAgent.js:4468)
  4. This is outside the RetryManager scope — caught by the outer catch at line 4711
  5. isRetryableError() checks the error message against DEFAULT_RETRYABLE_ERRORS, but "No output generated" doesn't match any pattern (429, rate_limit, timeout, etc.)
RetryManager.executeWithRetry(streamText(...))  ← retry wraps this
  → streamText() returns immediately (OK)       ← no error here
  → await result.steps                           ← NoOutputGeneratedError thrown HERE
  → caught at outer catch (line 4711)            ← NOT retried
  → "Error: Failed to get response from AI model. No output generated."

Suggested Fix

Two changes needed:

1. Add "No output generated" to DEFAULT_RETRYABLE_ERRORS

In RetryManager.js:

const DEFAULT_RETRYABLE_ERRORS = [
  'Overloaded',
  'overloaded',
  'rate_limit',
  'rate limit',
  '429',
  '500',
  '502',
  '503',
  '504',
  'timeout',
  'ECONNRESET',
  'ETIMEDOUT',
  'ENOTFOUND',
  'api_error',
  'No output generated',  // ← add this
];

2. Wrap await result.steps in a retry loop

In ProbeAgent.js around lines 4464-4488, the executeAIRequest function should catch NoOutputGeneratedError from await result.steps and re-invoke streamTextWithRetryAndFallback:

const executeAIRequest = async () => {
  const maxEmptyRetries = 2;
  for (let emptyRetry = 0; emptyRetry <= maxEmptyRetries; emptyRetry++) {
    const result = await this.streamTextWithRetryAndFallback(streamOptions);
    // ... timeout setup ...
    try {
      const steps = await result.steps;
      if (!steps || steps.length === 0) {
        if (emptyRetry < maxEmptyRetries) {
          console.log(`[WARN] Empty stream (0 steps), retrying (${emptyRetry + 1}/${maxEmptyRetries})...`);
          continue;
        }
      }
      // ... rest of text extraction ...
      return { finalText, result };
    } catch (err) {
      if (err?.name === 'AI_NoOutputGeneratedError' && emptyRetry < maxEmptyRetries) {
        console.log(`[WARN] NoOutputGeneratedError, retrying (${emptyRetry + 1}/${maxEmptyRetries})...`);
        continue;
      }
      throw err;
    }
  }
};

Impact

Any transient provider hiccup that returns an empty stream (common with Gemini under load) kills the entire task with no retry. This is especially bad for simple queries that would succeed on retry.

Trace Evidence

ai.request.started:  model=gemini-3.1-pro-preview, input_length=13316
ai.request.failed:   error="No output generated. Check the stream for errors."
visor.ai_check.failed: error="Error: Failed to get response from AI model. No output generated."
visor.check.generate-response.failed: error="The AI provider rate limit was reached..."

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexternal

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions