rohitg00/agentmemory · error · Error

OpenAI returned unexpected response: ${JSON.stringify(data).

Error message

OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}

What it means

After a 2xx response, OpenAIProvider.call expects choices[0].message.content (or, per issue #627, reasoning / reasoning_content fields emitted by DeepSeek V4, Qwen3, GLM, Kimi and older o-series). If none of these fields is present it throws with a 200-char JSON dump of the actual body. This catches protocol drift in OpenAI-compatible endpoints that return 200 with an unexpected shape.

Source

Thrown at src/providers/openai.ts:148

    const data = (await response.json()) as {
      choices?: Array<{
        message?: { content?: string; reasoning?: string; reasoning_content?: string };
      }>;
    };
    const message = data.choices?.[0]?.message;
    const content = message?.content;
    if (content) {
      return content;
    }
    // Fallback: some thinking models return reasoning but no content.
    // DeepSeek V4 / Qwen3 / GLM / Kimi return `reasoning_content`;
    // older OpenAI o-series + some compatibles return `reasoning`. #627
    const reasoning = message?.reasoning ?? message?.reasoning_content;
    if (reasoning) {
      return reasoning;
    }
    throw new Error(
      `OpenAI returned unexpected response: ${JSON.stringify(data).slice(0, 200)}`,
    );
  }
}

// Resolves the outbound-fetch timeout for the OpenAI LLM path.
// Precedence (preserving v0.9.17 behaviour):
//   1. OPENAI_TIMEOUT_MS       — OpenAI-scoped alias (back-compat)
//   2. AGENTMEMORY_LLM_TIMEOUT_MS — global LLM/embedding timeout (#446)
//   3. 60 000 ms default
function resolveTimeout(): number {
  const openaiRaw = getEnvVar("OPENAI_TIMEOUT_MS");
  const openai = parsePositiveInt(openaiRaw);
  if (openai !== undefined) return openai;

  const globalRaw = getEnvVar("AGENTMEMORY_LLM_TIMEOUT_MS");
  const globalMs = parsePositiveInt(globalRaw);
  if (globalMs !== undefined) return globalMs;

View on GitHub (pinned to e04ba88819)

Solutions

  1. Read the 200-char JSON dump in the message to see the actual response shape
  2. If content is filtered/empty, adjust the prompt or model to avoid the filter
  3. If using a compatible gateway, ensure it proxies real Chat Completions responses (choices[0].message.content)
  4. Check the endpoint is a chat/completions URL, not embeddings or /responses
  5. File/patch support for the new field name alongside reasoning/reasoning_content in openai.ts

Example fix

// before
baseUrl: 'https://api.example.com/v1/embeddings' // 200 but no choices -> unexpected response

// after
baseUrl: 'https://api.example.com/v1/chat/completions'
Defensive patterns

Strategy: type-guard

Validate before calling

assert(config.baseUrl?.includes('/chat/completions') ?? true, 'OpenAI provider baseUrl must point at a chat/completions endpoint');

Type guard

interface ChatCompletionResponse { choices?: Array<{ message?: { content?: string; reasoning?: string; reasoning_content?: string } }> }
function hasText(r: ChatCompletionResponse): boolean {
  const msg = r.choices?.[0]?.message;
  return !!msg && !!(msg.content ?? msg.reasoning ?? msg.reasoning_content);
}

Try / catch

try {
  return await provider.call(prompt);
} catch (e) {
  if ((e as Error).message.startsWith('OpenAI returned unexpected response')) {
    console.error('Endpoint returned a non-chat-completions 200 body; check baseUrl/model');
    return await fallbackProvider.call(prompt);
  }
  throw e;
}

Prevention

When it happens

Trigger: compress()/summarize() against an OpenAI-compatible endpoint whose 200 response lacks choices[0].message.content, reasoning, and reasoning_content — e.g. an empty choices array because content was filtered, a chat-completions-shaped call against an embeddings/responses endpoint, or a compat server returning a novel schema.

Common situations: Pointing the OpenAI provider at a third-party compatible gateway that changed its response schema; calling the new OpenAI Responses API path instead of Chat Completions; safety filters returning empty content with finish_reason 'content_filter'; reasoning-only responses whose content field is null on an endpoint not covered by the known reasoning aliases.

Related errors


AI-assisted analysis of rohitg00/agentmemory@e04ba88819 (2026-08-30). Data as JSON: /api/errors/7613ab70d50f2428. Report an issue: GitHub.