santifer/career-ops · warning · Error

Empty response

Error message

Empty response

What it means

Thrown when a completions response parses successfully but `data.choices[0].message.content` is undefined or an empty string. A 200 response with no generated content usually means the model produced no output (e.g. all tokens went to a reasoning/none finish, the provider stripped content, or output was filtered).

Solutions

  1. Retry — the rotation should blacklist the empty-responder and try the next free model.
  2. Log the full data.choices[0] object once to see whether content lives in a different field for that model.
  3. Lower max_tokens constraints or check finish_reason; 'length' with no content means the prompt consumed the whole budget.
  4. Exclude chronically empty models from PROVIDER_PRIORITY or blacklist them persistently.
  5. If a specific model repeatedly returns empty, pin a different model instead of relying on rotation.

Example fix

// before
const content = data.choices?.[0]?.message?.content ?? '';
if (!content) throw new Error('Empty response');
// after
const choice = data.choices?.[0];
const content = choice?.message?.content ?? '';
if (!content) {
  throw new Error(`Empty response (finish_reason=${choice?.finish_reason ?? 'n/a'}, model=${data.model ?? 'unknown'})`);
}
Defensive patterns

Strategy: retry

Type guard

function hasContent(data) {
  const c = data?.choices?.[0]?.message?.content;
  return typeof c === 'string' && c.length > 0;
}

Try / catch

try {
  result = await callOpenRouter(systemPrompt, userMessage);
} catch (err) {
  if (err.message === 'Empty response' && attempt < MAX_ATTEMPTS) {
    attempt++; // rotation will try the next free model
    return callWithRotation(systemPrompt, userMessage, attempt);
  }
  throw err;
}

Prevention

When it happens

Trigger: data.choices is an empty array, the choice has no message, or message.content === '' — after a successful 200 and no data.error.

Common situations: Some free models return choices with content in nonstandard fields or empty content when the finish reason is 'length' with zero produced tokens; provider-side content filtering; model returning only a tool/reasoning block.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/b341c7b1d3731657. Report an issue: GitHub.

Appendix: source

Thrown at openrouter-runner.mjs:264

      const resp = await fetch(OPENROUTER_API_URL, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${key}`,
          'Content-Type':  'application/json',
          'HTTP-Referer':  'https://github.com/career-ops-hq/career-ops',
          'X-Title':       'career-ops',
        },
        body,
        signal: ctrl.signal,
      });
      if (!resp.ok) {
        const t = await resp.text();
        throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);
      }
      const data = await resp.json();
      if (data.error) throw new Error(data.error.message);
      const content = data.choices?.[0]?.message?.content ?? '';
      if (!content) throw new Error('Empty response');
      console.log('OK');
      const usage = normalizeOpenAIUsage(data.usage);
      return { content, usage };
    } catch (e) {
      if (e.name === 'AbortError') throw new Error(`Pinned model timed out after ${MODEL_TIMEOUT_MS / 1000}s`);
      throw e;
    } finally {
      clearTimeout(timerId);
    }
  }

  const models = await loadFreeModels();
  let lastError;

  if (models.length === 0) {
    throw new Error(
      'No free OpenRouter models are available. Model loading may have failed, or your account currently has no free models.'
    );

View on GitHub (pinned to aac998c7ed)