APIMart 流式输出怎么配置?从 stream:true 到 SSE 处理
简答:APIMart 流式输出的核心是在兼容 OpenAI 的聊天请求体中加入 <code>"stream": true</code>,然后按 SSE 事件逐段读取增量内容。先确认当前文档给出的 Base URL、API Key 和模型 ID,再用最小请求测试;如果服务端返回完整 JSON、浏览器一直等待或连接中断,应依次检查请求参数、响应类型、代理缓冲和超时设置。
定义:流式输出是指模型生成内容后立即通过 Server-Sent Events(SSE)分段返回,而不是等待完整答案生成后一次性返回。
先试这一步:确认配置与最小请求
本文是第三方 AI API 接入指南,不代表 APIMart 官方。APIMart 的公开页面将其描述为提供 OpenAI 兼容接口的 AI API 聚合层;公开文章示例使用 https://api.apimart.ai/v1 作为 Base URL,但具体可用地址、模型 ID、权限和状态仍应以当前文档或实际响应为准。可先查看 APIMart 页面和当前接口文档,避免把旧地址直接用于生产环境。
- 准备一个 API Key,并只放在服务端环境变量中,不要写入浏览器代码或提交到 Git。
- 复制当前文档显示的 Base URL,确认不要重复拼接
/v1。 - 从模型页面复制精确的模型 ID,不要根据展示名称自行猜测。
- 在请求体加入
stream: true,并确认请求地址是聊天接口而不是图像、视频或异步任务接口。 - 成功判定不是“收到了 HTTP 200”这么简单,还要确认响应类型接近
text/event-stream,并能读取至少一段增量文本。
| 检查项 | 正确方向 | 常见错误 |
|---|---|---|
| Base URL | 使用当前文档提供的地址 | 把完整接口地址再次拼接 |
| 认证 | Authorization: Bearer YOUR_API_KEY | Key 为空、过期或带多余引号 |
| 请求参数 | stream: true | 写成字符串或放在错误层级 |
| 模型 | 使用目录中显示的精确 ID | 使用营销名称或已下架 ID |
| 响应处理 | 按 SSE 事件逐段读取 | 直接调用一次性 JSON 解析 |
curl、Python 和 Node.js 配置示例
下面示例采用 OpenAI 兼容的 Chat Completions 结构。YOUR_BASE_URL、YOUR_API_KEY 和 YOUR_MODEL_ID 都是占位符,实际值必须从当前文档或模型页面核对。公开资料说明,兼容接口可以通过 stream: true 返回流式内容;OpenAI API 参考也将流式事件列为接口能力之一,具体字段仍应以实际网关响应为准。参考:APIMart 统一 LLM API 示例、Chat API 参考。
curl:先验证是否真的返回流
curl -N "YOUR_BASE_URL/chat/completions" -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{"model":"YOUR_MODEL_ID","messages":[{"role":"user","content":"用一句话解释 SSE"}],"stream":true}'
-N用于减少 curl 对输出的缓冲。若终端仍然只在最后显示完整结果,先不要判断接口不支持流式,继续检查网关、反向代理或客户端缓冲。
Python:逐行读取 SSE 数据
import os
import json
import requests
url = os.environ["APIMART_BASE_URL"].rstrip("/") + "/chat/completions"
headers = {"Authorization": f"Bearer {os.environ['APIMART_API_KEY']}", "Content-Type": "application/json"}
body = {"model": os.environ["APIMART_MODEL"], "messages": [{"role": "user", "content": "用一句话解释 SSE"}], "stream": True}
with requests.post(url, headers=headers, json=body, stream=True, timeout=(10, 180)) as response:
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
try:
event = json.loads(data)
text = event.get("choices", [{}])[0].get("delta", {}).get("content") or ""
print(text, end="", flush=True)
except json.JSONDecodeError:
print("收到非 JSON SSE 数据,需记录原始行并核对网关格式", flush=True)
Node.js:使用 fetch 读取增量字节
const response = await fetch(`${process.env.APIMART_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.APIMART_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: process.env.APIMART_MODEL,
messages: [{ role: "user", content: "用一句话解释 SSE" }],
stream: true
})
});
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (data === "[DONE]") continue;
try {
const chunk = JSON.parse(data);
process.stdout.write(chunk.choices?.[0]?.delta?.content || "");
} catch {
console.error("非 JSON SSE 数据:", data);
}
}
}
SSE 怎么解析:增量拼接、结束标记与前端转发
SSE 本质上是服务器持续发送的事件流,客户端不能把整个响应当作一个普通 JSON 一次性解析。浏览器端常见的 SSE 响应类型是 text/event-stream,事件通常按空行分隔,数据行以 data: 开头。可参考 MDN 的 Server-sent events 说明。
- 读取每个完整事件,而不是每个网络数据包;一个 JSON 事件可能被拆成多个字节块。
- 解析
data:后面的 JSON,并从兼容 Chat Completions 的响应中读取增量文本字段。 - 将每次得到的文本追加到同一个缓冲区,再实时渲染到页面。
- 遇到
[DONE]时结束读取;如果实际网关返回其他结束结构,应以原始响应为准。 - 工具调用或结构化 JSON 可能被拆成多个片段,不能在收到第一片段时就直接执行或解析,应该先缓存,直到字段完整。
如果浏览器通过自己的后端代理接收流,后端应转发流式响应,而不是先等待上游完整结束。通常需要保持 Content-Type: text/event-stream,并根据部署环境设置 Cache-Control: no-cache;使用 Nginx 时可考虑关闭响应缓冲,例如 X-Accel-Buffering: no。这些设置解决的是代理层交付问题,不代表模型本身一定支持流式输出。
前端的“停止生成”按钮应取消浏览器到后端的请求;后端还要把取消信号继续传递给上游请求。否则页面虽然停止显示,服务端可能仍在继续生成并产生用量。取消后的计费和 usage 返回方式没有在本次公开资料中得到统一确认,应以当前接口响应和计费说明为准。
哪些接口适合流式输出?哪些不适合?
| 场景 | 通常的处理方式 | 核验重点 |
|---|---|---|
| 聊天文本 | 使用聊天接口并设置 stream: true | 模型 ID、SSE 字段、结束标记 |
| 工具调用或 Agent | 流式传输文本和事件,工具参数先缓存 | 事件顺序、JSON 是否完整、重复执行风险 |
| 图像生成 | 通常先提交生成请求,再等待结果或任务状态 | 是否返回任务 ID,是否另有轮询接口 |
| 视频生成 | 通常采用异步任务流程 | 任务状态、结果地址和有效期 |
| 需要完整 JSON 的后台任务 | 优先使用同步响应或异步任务 | 结构化输出和失败重试策略 |
不要因为平台提供统一 API,就默认所有模型和所有接口都支持相同的流式字段。APIMart 页面同时列出聊天、图像、视频和音频等不同能力,支持范围与返回格式应按具体模型文档确认。对模型支持、限速、区域可用性等问题,公开支持页面也建议按具体模型和请求情况核对,参考 APIMart Support。
流式输出失败时,按这个顺序排查
- 401 或 403:确认 Key 是否为空、是否包含多余空格、是否使用了正确的 Bearer 格式;再检查请求是否发到了对应 Base URL。
- 404:打印最终请求地址,检查是否重复拼接
/v1、是否误用了模型页面地址,或当前网关实际使用的是其他路径。 - 400:先删除可选参数,只保留
model、messages和stream,确认请求体是合法 JSON。 - 返回完整 JSON:检查
stream是否为布尔值true,并确认当前模型和接口真的支持流式;同时检查客户端是否自动聚合响应。 - 浏览器一直等待:检查后端是否设置流式响应头,Nginx、CDN 或框架是否启用了缓冲;用 curl 直接请求区分上游问题和前端代理问题。
- 429:降低并发,记录响应头和请求 ID,采用带上限的指数退避;不要瞬间重复发送大量相同请求。
- 5xx 或超时:区分首字节前超时与输出中途断开,设置合理的连接超时和读取超时,保留已收到的文本,并只对可重试错误重试。
- 流中断:保存部分内容和请求日志,避免把半截 JSON 当成完整结果;重新发起请求时应考虑续写提示,而不是盲目重复执行工具调用。
排查时建议记录 HTTP 状态码、最终 URL、模型 ID、响应头、首字节时间、已接收字符数和请求 ID,但必须脱敏 API Key、用户隐私和完整提示词。
常见问题
APIMart 流式输出只需要加 stream:true 吗?
对兼容 OpenAI 的聊天接口来说,stream:true 是核心开关,但还必须确认 Base URL、模型 ID、鉴权方式和客户端读取逻辑。若代理层缓冲响应,服务端可能已经在发送流,浏览器却仍然看不到增量内容。
为什么 curl 能看到流,网页却要等全部生成完?
通常是后端代理、Nginx、CDN 或框架默认缓冲导致。先确认后端返回 text/event-stream,再检查是否设置了 no-cache 和关闭代理缓冲,最后确认前端没有把响应整体读取后才渲染。
流式响应里的内容应该读取哪个字段?
兼容 Chat Completions 的响应通常会把增量文本放在 choices 中的 delta.content,但不同网关或模型可能返回不同事件结构。应先打印一条脱敏原始事件,再根据实际结构编写解析器,不要把单一字段泛化到所有接口。
图片和视频接口也能使用 stream:true 吗?
不能默认这样处理。聊天文本适合逐段输出,图像和视频通常采用异步任务或结果查询流程。是否支持流式、是否返回 task_id,需要查看具体模型接口说明和实际响应。
流式输出会不会改变计费或 usage 返回方式?
流式主要改变交付方式,不应直接推断为改变计费规则。但 usage 可能在最后一个事件、响应头或其他字段中返回,取消请求后的计费处理也可能取决于网关实现,应以当前计费说明和实际响应为准。
相关阅读
- 先看 APIMart OpenAI 兼容 API 的整体接入方法
- 再核对 API Key、环境变量与 Base URL 配置
- 如果返回 401,按 Key、Bearer 和地址逐项排查
- 遇到 429 时查看限流、配额和重试策略
- 流式连接超时可继续查看专门排查步骤
参考资料
- APIMart 中文页面:OpenAI 兼容 API 与模型能力介绍
- APIMart:一个 API 调用所有 LLM(apimart.ai)
- APIMart Support(apimart.ai)