跳转至

流式响应

POST /v1/chat/completionsPOST /v1/responses 在请求中设置 "stream": true 后返回 Server-Sent Events(SSE)。响应的 Content-Typetext/event-stream

流式响应不是一个 JSON 文档:服务器会持续发送事件,每个事件可能只包含一小段增量内容。客户端必须逐行读取、按空行分隔事件,并分别解析每个事件的 data。正常结束通常以 data: [DONE] 表示;Responses 的事件还可能带有协议定义的事件类型和结构。

PowerShell / cURL

PowerShell 中使用 curl.exe -N 禁止缓冲输出。Chat Completions 示例:

$body = '{"model":"gpt-5.6-terra","messages":[{"role":"user","content":"Reply with OK"}],"stream":true}'
curl.exe -N https://api.yuniversity.cc/v1/chat/completions `
  -H "Authorization: Bearer $env:YUNIVERSITY_API_KEY" `
  -H "Content-Type: application/json" `
  -d $body

Responses 示例:

$body = '{"model":"gpt-5.6-terra","input":"Reply with OK","stream":true,"store":false}'
curl.exe -N https://api.yuniversity.cc/v1/responses `
  -H "Authorization: Bearer $env:YUNIVERSITY_API_KEY" `
  -H "Content-Type: application/json" `
  -d $body

Python(标准库)

urllib.request 的响应对象可以逐行迭代。下面的示例保留 SSE 的 data: 行,并在收到 data: [DONE] 后停止:

import json
import os
from urllib.request import Request, urlopen

payload = {
    "model": "gpt-5.6-terra",
    "messages": [{"role": "user", "content": "Reply with OK"}],
    "stream": True,
}
request = Request(
    "https://api.yuniversity.cc/v1/chat/completions",
    data=json.dumps(payload).encode(),
    headers={
        "Authorization": f"Bearer {os.environ['YUNIVERSITY_API_KEY']}",
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
)
with urlopen(request) as response:
    for raw_line in response:
        line = raw_line.decode("utf-8").rstrip("\r\n")
        if not line.startswith("data: "):
            continue
        data = line[6:]
        if data == "[DONE]":
            break
        event = json.loads(data)
        print(event)

真实客户端应按空行组装完整事件;一个事件可能包含多行 data:,不能假设每一行都是独立 JSON。

JavaScript fetch

ReadableStream 返回的是字节块,不保证每块恰好对应一个事件。以下代码通过 TextDecoder 和缓冲区按空行处理事件,并使用 AbortSignal.timeout 设置客户端超时:

const response = await fetch("https://api.yuniversity.cc/v1/responses", {
  method: "POST",
  signal: AbortSignal.timeout(120000),
  headers: {
    "Authorization": `Bearer ${process.env.YUNIVERSITY_API_KEY}`,
    "Content-Type": "application/json",
    "Accept": "text/event-stream",
  },
  body: JSON.stringify({
    model: "gpt-5.6-terra",
    input: "Reply with OK",
    stream: true,
    store: false,
  }),
});

if (!response.ok || !response.body) {
  throw new Error(`HTTP ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finished = false;
while (!finished) {
  const { value, done } = await reader.read();
  buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
  const events = buffer.split("\n\n");
  buffer = events.pop() || "";
  for (const event of events) {
    for (const line of event.split("\n")) {
      if (!line.startsWith("data: ")) continue;
      const data = line.slice(6);
      if (data === "[DONE]") {
        finished = true;
        break;
      }
      console.log(JSON.parse(data));
    }
    if (finished) break;
  }
  if (done) break;
}
await reader.cancel();

浏览器前端不应直接暴露 API Key;把此类请求放在受保护的后端。调用 reader.cancel() 或断开网络只表示客户端停止读取,不能保证上游请求已经在计费前取消。需要可靠的取消和费用判断时,应以服务端最终状态和用量信息为准。

处理建议

  • 先检查 Content-Type,再进入 SSE 解析器;非 2xx 响应应按普通错误响应读取。
  • 保存未完成的字节和事件,直到遇到空行;不要对每个网络 chunk 直接调用 JSON 解析。
  • 将增量文本追加到本地缓冲区,并在 data: [DONE] 或协议完成事件后结束 UI 状态。
  • 网络断开时记录请求 ID(不要记录 API Key),并避免在请求是否已被接受不明确时盲目重试。