> ## Documentation Index
> Fetch the complete documentation index at: https://docs.playapi.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Responses WebSocket 接入

> 在一个连接中完成两轮对话，校验完整结果并回放历史。

此路径使用 `GET /v1/responses` 进行 WebSocket 升级。先确认模型、上游和渠道支持 Responses WebSocket，管理员需开启渠道 `ResponsesWebSocketEnabled`。普通 HTTP Responses 可用不能证明双向连接也可用。

## 可运行的两轮文本示例

按[连接与运行环境](/zh/guides/websocket)安装依赖、设置环境变量。运行：

```sh theme={null}
node examples/responses-websocket.mjs
```

脚本依次请求咖啡店名称与解释；收到并校验第一轮完成事件后，才发送第二轮。输出按轮发布，第二轮失败时第一轮已发布文本仍保留。复制到其他目录时同时保留 `websocket-client.mjs`。

```javascript theme={null}
import { openWebSocket } from './websocket-client.mjs';

let session;
try {
  session = await openWebSocket('responses');
  const history = [];
  const seenResponses = new Set();
  for (const prompt of ['Suggest a short name for a coffee shop.', 'Explain that name in one sentence.']) {
    history.push({ role: 'user', content: prompt });
    await session.send({
      type: 'response.create', model: session.model,
      input: history, instructions: 'Answer briefly in plain text.',
      store: false, include: ['reasoning.encrypted_content'],
    });
    let responseId;
    const textParts = new Map();
    const itemIds = new Map();
    let textBytes = 0;
    for (;;) {
      const event = await session.receive();
      if (event.type === 'response.created') {
        const id = event.response?.id;
        if (responseId || typeof id !== 'string' || !id || seenResponses.has(id)) throw new Error('Invalid response identity.');
        responseId = id;
        seenResponses.add(id);
      } else if (event.type === 'response.output_text.delta') {
        if (!responseId || (event.response_id && event.response_id !== responseId) || typeof event.delta !== 'string'
            || typeof event.item_id !== 'string' || !event.item_id
            || !Number.isInteger(event.output_index) || event.output_index < 0
            || !Number.isInteger(event.content_index) || event.content_index < 0) throw new Error('Invalid text delta.');
        const key = `${event.output_index}:${event.content_index}`;
        if (itemIds.has(event.output_index) && itemIds.get(event.output_index) !== event.item_id) throw new Error('Mixed output items.');
        itemIds.set(event.output_index, event.item_id);
        textParts.set(key, (textParts.get(key) ?? '') + event.delta);
        textBytes += Buffer.byteLength(event.delta);
        if (textBytes > 1024 * 1024) throw new Error('Text limit exceeded.');
      } else if (['response.completed', 'response.done'].includes(event.type)) {
        const result = event.response;
        if (!responseId || result?.id !== responseId || result.status !== 'completed' || result.error
            || !Array.isArray(result.output) || !result.output.length) throw new Error('Response did not complete.');
        const finalParts = new Map();
        for (const [index, item] of result.output.entries()) {
          if (item.type === 'reasoning') continue;
          if (item.type !== 'message' || item.id !== itemIds.get(index) || item.role !== 'assistant' || item.status !== 'completed'
              || !Array.isArray(item.content) || !item.content.length) throw new Error('Unsupported output or tool call.');
          for (const [partIndex, part] of item.content.entries()) {
            if (part.type !== 'output_text' || typeof part.text !== 'string') throw new Error('Refusal or invalid text.');
            finalParts.set(`${index}:${partIndex}`, part.text);
          }
        }
        if (finalParts.size !== textParts.size
            || [...finalParts].some(([key, text]) => textParts.get(key) !== text)) throw new Error('Final text differs from deltas.');
        const text = [...finalParts.values()].join('\n');
        if (!text.trim()) throw new Error('Empty response.');
        // Replay every raw output item, preserving encrypted reasoning and metadata.
        history.push(...result.output);
        console.log(text);
        break;
      } else if (['response.failed', 'response.incomplete', 'response.cancelled', 'response.canceled'].includes(event.type)) {
        throw new Error('Response ended without success.');
      } else if (event.response_id && event.response_id !== responseId) {
        throw new Error('Unexpected response identity.');
      }
    }
  }
} catch {
  console.error('Responses WebSocket failed. Check configuration, handshake, events, and deadline; no automatic retry.');
  process.exitCode = 1;
} finally {
  session?.close();
}
```

## 历史、增量与完成

示例显式重放 user 消息和全部原始 output，包括推理密文、消息 phase 和额外元数据；每轮重新发送 instructions。模型必须支持 `reasoning.encrypted_content`，不支持时应按目标协议调整，而不是丢弃返回的推理状态。

接收器先取得 `response.created` 的 ID，按 output\_index / content\_index / item\_id 关联文本。Responses 文本增量不保证带 response\_id；出现该字段时才额外核对。最终完成对象必须与当前响应匹配，其文本必须与已收增量一致。示例拒绝空输出、拒答、工具调用及未完成消息，不把它们当作文本答案。

需要使用 `previous_response_id` 时，发送新输入并确认上游连接状态仍有效；重连后不能假设缓存保留。无状态恢复可重放完整历史或[压缩窗口](/zh/guides/compaction)，但应由应用决定是否重发。官方协议背景见 [WebSocket mode](https://developers.openai.com/api/docs/guides/websocket-mode)。

## 当前网关与上游能力不同

项目每连接只允许一个活动生成，并锁定已建立连接的模型和渠道。不能照搬官方最新文档的多 stream\_id 并发、排队和分叉承诺。切换模型请新建连接；增量续接是否可用需真实渠道验证。握手和每次 create 都可能因权限、限流或渠道配置变化失败。

`generate: false` 预热和额外供应商字段不在示例范围。`stream`、`background` 不作为 WebSocket create 的传输开关。客户端函数工具需要单独处理工具参数、执行结果和恢复策略；本示例主动拒绝工具输出。

## 取消和恢复

API 接受活动生成的 `response.cancel` 控制事件，但成功取决于上游响应。控制错误与生成错误需分别关联；不要只根据发出 cancel 就确认取消。示例未自动发送取消，超时后关闭连接并非保证退款或中止上游。

[握手、字段和事件参考](/zh/api-reference/responses-websocket) · [排障](/zh/guides/troubleshooting)

[Read in English](/en/guides/responses-websocket)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.