linshenkx/prompt-optimizer · error · APIError

API returned invalid response: choices is empty or missing

Error message

API returned invalid response: choices is empty or missing

What it means

After a non-streaming chat-completions call, the response JSON had no choices array (or an empty one), so the adapter cannot extract a completion and throws. The HTTP call itself succeeded; the payload is invalid for the chat-completions contract.

Source

Thrown at packages/core/src/services/llm/adapters/openai-adapter.ts:1148

      callbacks.onError(error instanceof Error ? error : new Error(String(error)))
      throw error
    }
  }

  protected async parseCompletionResponse(response: any, modelId: string): Promise<LLMResponse> {
    // 处理原始 SSE 字符串响应(某些 API 返回未解析的 SSE 格式)
    if (typeof response === 'string') {
      return this.parseSSEResponse(response, modelId)
    }

    // 检测是否为流式响应(某些 API 强制返回流式响应)
    if (this.isStreamResponse(response)) {
      return await this.consumeStreamResponse(response as AsyncIterable<any>, modelId)
    }

    // 处理响应中的 reasoning_content 和普通 content
    if (!response.choices || response.choices.length === 0) {
      throw new APIError('API returned invalid response: choices is empty or missing')
    }

    const choice = response.choices[0]
    if (!choice?.message) {
      throw new APIError('No valid response received')
    }

    let content = choice.message.content || ''
    let reasoning = ''

    // 处理推理内容(如果存在)
    // SiliconFlow 等提供商在 choice.message 中并列提供 reasoning_content 字段
    if ((choice.message as any).reasoning_content) {
      reasoning = (choice.message as any).reasoning_content
    } else {
      // 检测并分离content中的think标签
      const processed = this.processThinkTags(content)
      content = processed.content

View on GitHub (pinned to 3e677b1d9f)

Solutions

  1. Log the full response body to see what the server actually returned
  2. Verify the request went to the chat-completions (or correct) endpoint for that adapter
  3. If a gateway wraps responses, unwrap/fix it or set the right baseURL
  4. Retry once — truncated responses happen under proxy timeouts

Example fix

// before
const text = await adapter.sendMessage(msgs)

// after
let text
try { text = await adapter.sendMessage(msgs) }
catch (e) {
  if (e instanceof APIError && /choices is empty/.test(e.message)) {
    text = await retryOnce(() => adapter.sendMessage(msgs))
  } else throw e
}
Defensive patterns

Strategy: validation

Validate before calling

const r = await fetch(`${baseURL}/chat/completions`, opts)
const body = await r.json()
if (!Array.isArray(body?.choices) || body.choices.length === 0) throw new Error('Server returned no choices — check endpoint/gateway')

Type guard

function hasChoices(r: unknown): r is { choices: { message?: { content?: string } }[] } {
  return Array.isArray((r as any)?.choices) && (r as any).choices.length > 0
}

Try / catch

try { return await adapter.sendMessage(msgs) }
catch (e) {
  if (e instanceof APIError && /choices is empty/.test(e.message)) return retryOnce(() => adapter.sendMessage(msgs))
  throw e
}

Prevention

When it happens

Trigger: OpenAI-compatible server returning {error: ...} with 200, a truncated/empty body, embedding-style responses from wrong endpoints, or gateways that wrap completions differently.

Common situations: Gateways returning their own envelope objects, pointing chat calls at /v1/embeddings or models endpoint, JSON cut by proxy buffering, providers that omit choices on content-filter blocks.

Related errors


AI-assisted analysis of linshenkx/prompt-optimizer@3e677b1d9f (2026-08-27). Data as JSON: /api/errors/91fdfa6e30af11b4. Report an issue: GitHub.