一条能跑的 curl
curl -N -sS https://ai.idcidiot.com/v1/chat/completions \
-H "Authorization: Bearer $YUN25_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"model": "glm-5.3-flash",
"stream": true,
"messages": [{"role": "user", "content": "写一个 bash 函数,列出目录里最大的 5 个文件"}]
}'-N 等于 --no-buffer,关掉 curl 自己的输出缓冲。不加这个参数,返回会被攒在缓冲区里,看起来像卡住,其实数据早就到了。-sS 隐藏进度条但保留错误信息,脚本里比默认输出干净。Accept 头不是必须的,写上便于中间层判断这是流式请求。Windows 的 PowerShell 里 curl 是 Invoke-WebRequest 的别名,参数对不上,要显式写 curl.exe。
返回长什么样
正常输出是一串以 data: 开头的行,事件之间用空行分隔:
data: {"id":"chatcmpl-8f2","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"可"},"finish_reason":null}]}
data: {"id":"chatcmpl-8f2","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"以"},"finish_reason":null}]}
data: [DONE]每一行是一块增量,delta.content 拼起来才是完整回答。第一块通常只带 delta.role,没有正文,取值时允许空。data: 后面有一个空格,解析要连空格一起去掉;[DONE] 不是 JSON,按字符串比较。中间可能夹着以 : 开头的注释行,那是心跳,跳过即可。
每块都带 id、object、created、model 和 choices。object 固定是 chat.completion.chunk,可以用它确认对面返回的确实是流式响应,而不是一段普通 JSON。choices[0].index 在 n 大于 1 时区分候选分支,单候选时永远是 0。
函数调用的流式块里 delta.tool_calls 会分片到达,要按 index 把参数拼起来,比纯文本流麻烦。只想验证连通性的话,先用纯文本提示词,别一上来就测工具调用。
把 content 抽出来
curl -sN https://ai.idcidiot.com/v1/chat/completions \
-H "Authorization: Bearer $YUN25_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"glm-5.3-flash","stream":true,"messages":[{"role":"user","content":"用一句话解释 TCP 三次握手"}]}' \
| sed -n 's/^data: //p' \
| while IFS= read -r line; do
[ "$line" = "[DONE]" ] && break
printf '%s' "$line" | jq -r '.choices[0].delta.content // empty'
donesed 去掉前缀,jq 取字段,// empty 让 null 和缺失字段都输出空字符串。没有 jq 就装一个,用 cut 或字符串截取很容易被正文里的引号打乱。
想同时拿 token 统计,在请求体里加 "stream_options": {"include_usage": true},末尾一块会带 usage,前面所有块的统计都是 null。按输出逐字计费的场景,这个字段比事后估算准。
自己写客户端要注意什么
用程序消费 SSE 时,三件事容易做错。按行读取而不是按字节读,一个网络包里可能只有半行,缓冲区要一直读到换行为止。每个事件以空行结束,也就是连续两个换行;只读到一行就开始解析,JSON 会被截断。连接要保持长,不少 HTTP 客户端默认在首包后关闭,需要显式关掉空闲超时。
如果返回头是 Content-Type: application/json 而不是 text/event-stream,说明请求没被当成流式,多半是 body 里少了 stream 字段,或者打到了非流式端点。先看响应头,再看内容,比逐行猜快。
断线重连时服务端不会从断点续传,只能整段重发。长回答在客户端做超时兜底,比反复重连划算,重连几次的 token 就够再生成一遍了。
常见坑
- 输出一次性刷出来:代理或 CDN 缓冲了响应,关掉缓冲或换直连。
- 只收到
[DONE]:响应里没有正文,检查max_tokens是不是 0,或者模型直接返回了空内容。 400且提示 JSON 解析失败:-d里的引号被 shell 吃掉,用单引号包住整个 JSON。- 中文乱码:终端不是 UTF-8,
export LANG=C.UTF-8或换终端。 - 请求挂住不返回:加
--max-time 120和--connect-timeout 10。 - 管道里
jq报错:某一行不是合法 JSON,先打印原始行看看是不是心跳混进来了。 - 加了
-N还是卡:换--http1.1试一次,个别代理对 HTTP/2 的流式转发支持不好。
成本
glm-5.3-flash 的 model_ratio 是 0.4,官渠国模分组 0.2 倍,输入价 = 0.4 × 2 × 0.2 = $0.16/百万 token,输出按 completion_ratio 3.5 折算为 $0.56/百万 token。100 万输入加 20 万输出,合计 $0.16 + $0.112 = $0.27。
写脚本批量跑时,把 model 和提示词抽成变量,别在循环里拼长 JSON,引号转义出错很难查。偶尔手测用 curl 最快,接进业务代码还是交给 SDK 处理分帧和重连更省事。倍率表在 价格页,模型清单在 /model/。把上面那条命令存成 shell 函数,下次换提示词直接调用。