Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

eWCli

Erlang 原生 HTTP 客户端:HTTP/1.1、HTTP/2、HTTP/3、WebSocket,并针对大模型 API(SSE / NDJSON 流式、附件、Bearer、429 重试、长超时)做了传输层优化。

运行时依赖:OTP 标准库 + quic(纯 Erlang QUIC/HTTP3)。绑定 jsx 等 JSON 库,请求体由调用方编码。

功能概览

类别 能力
协议 HTTP/1.1、HTTP/2(ALPN)、HTTP/3(QUIC)、WebSocket
请求 同步一次性、连接池、流式收发、显式长连接
传输 TLS 1.2/1.3、Happy Eyeballs、Keep-Alive、H2 多路复用
代理 HTTP CONNECT、正向代理、SOCKS5/4、HTTP_PROXY / NO_PROXY
流式 SSE、NDJSON、原始 chunk、上传 chunked / H2 DATA、写文件 sink
大模型 postStreamstreamToFilefilePath 附件、bearer / apiKey、流式重试边界
其他 Cookie、multipart、gzip/deflate、可选的一次性 brotli 解码、证书 pinning

快速开始

rebar3 compile
rebar3 eunit
-include_lib("eWCli/include/wcCli.hrl").   %% 或项目里 -include("wcCli.hrl")

{ok, _} = application:ensure_all_started(eWCli).

{ok, #wcResp{status = 200, body = Body}} =
    eWCli:get(<<"https://example.com/">>).

本机自签证书(测试 Caddy)需要 #{verify => verify_none}

API 分层

应用层(Agent / 业务,自己拼 JSON)
    eWCli
      一次性:get / post / request / postMultipart
      流式:  postStream / streamToFile / requestStream
      长连接:open + connRequest + connStream
      HTTP/3:openH3 / h3Conn*
      WebSocket:connectWs + wsSend
    wcConn / wcH3Conn / wcPool / wcSse / wcNdjson / wcRetry / ...

响应是 #wcResp{}versionstatusreasonheadersbodytrailers)。请求头是 [{Name, Value}] 列表。

一次性请求

连接池自动借还,跟随重定向。

eWCli:get(Url).
eWCli:get(Url, Opts).
eWCli:post(Url, Headers, Body, Opts).
eWCli:request(Method, Url, Headers, Body, Opts).

同步响应默认最多攒 64MBmaxBody)。更大就调大该选项,或改用 streamToFile / withBody => false,避免把整段 body 留在堆上。

eWCli:get(Url, #{maxBody => 512 * 1024 * 1024}).
eWCli:get(Url, #{maxBody => infinity}).   %% 不截断,内存仍按实际体积增长

流式请求

postStream 默认:format => auto(按响应 Content-Type 选 sse / ndjson / raw)、recvTimeout => infinitymaxBody => infinitywithBody => false、首包/空闲超时 300s。

Handler 返回 continue | stopstop 会取消并得到 {error, cancelled})。

ok = eWCli:postStream(Url, Headers, JsonBody, #{
    bearer => <<"sk-...">>,
    json => true,
    handler => fun
        ({headers, 200, _, _, _}) -> continue;
        ({sse, {data, Bin}}) -> handleToken(Bin), continue;
        ({sse, done}) -> continue;
        (done) -> continue;
        (_) -> continue
    end
}).

分步(自己管连接):

{ok, Conn, Ref} = eWCli:requestStream(<<"POST">>, Url, Headers, Body, Opts),
eWCli:connStream(Conn, Ref, Handler, infinity, #{format => sse}),
eWCli:release(Conn).

显式连接

{ok, Conn} = eWCli:open(<<"api.openai.com">>, 443, #{tls => true}),
{ok, Ref} = eWCli:connRequest(Conn, <<"POST">>, Path, Headers, Body, #{}),
eWCli:connStream(Conn, Ref, Handler, infinity, #{format => sse}),
eWCli:close(Conn).

同一 Conn 上 HTTP/2 可并发多路请求。

WebSocket

{ok, Conn, Ref} = eWCli:connectWs(<<"wss://echo.example.com/ws">>),
eWCli:wsSend(Conn, text, <<"hello">>),
eWCli:close(Conn).

HTTP/3

true = eWCli:http3Available().
eWCli:get(Url, #{protocol => http3, verify => verify_none}).

默认 protocol => auto只在 TCP 上协商 HTTP/1.1 / HTTP/2,不自动升级 HTTP/3。 原因:QUIC/UDP 路径在部分 CDN 上存在「慢故障」(握手包被静默丢弃,只能等超时, 实测见过单请求 ~32s),默认档不给请求暴露这种风险。想让客户端自动升级 H3 时用 auto3:Alt-Svc 学到 h3 端点即升级,失败回退 TCP,连续 3 次失败熔断 5 分钟 (冷却期内不再尝试该主机的 H3)。显式 http3 则强制 QUIC,失败不回退、如实报错。

protocol 行为
auto(默认) 仅 TCP:h1 / h2(ALPN)
auto3 TCP + Alt-Svc 学到 h3 时自动升级,失败回退 + 熔断冷却
http3 强制 QUIC/HTTP/3

大模型 API

库只做 HTTP 传输(认证头、流解析、重试、背压、写盘),不封装各厂商 messages / tools schema。

场景对照

场景 API 要点
非流式 Chat / Embed / Tools post/4 bearerjsonretryidempotencyKey
SSE(OpenAI / DeepSeek / Anthropic) postStream/4 Content-Type: text/event-stream 时自动 SSE
NDJSON(Ollama /api/chat postStream/4 *json* Content-Type 时自动 NDJSON;也可显式 format => ndjson
慢思考(长 TTFT) postStream/4 firstByteTimeout;吐词卡顿用 chunkIdleTimeout
语音/图片下载 streamToFile/2,3 边收边写文件
STT / Files / 大附件 postMultipart/3 {filePath, ...} 按块读盘上传
取消生成 handler stopconnCancelStream/2
Azure post/4 azureApiKey
Gemini 等 post/4 apiKey => {<<"x-goog-api-key">>, Key}
Realtime connectWs/2 subProtocolwsDeflate
429 retry => #{maxAttempts => 3} 流式:2xx 且已开始收 body 后不再重试(避免重复计费)

非流式

{ok, #wcResp{status = 200, body = RespBin}} =
    eWCli:post(
      <<"https://api.openai.com/v1/chat/completions">>,
      [],
      JsonBody,   %% 调用方 jsx:encode / thoas 等
      #{
        bearer => ApiKey,
        json => true,
        recvTimeout => 120000,
        retry => #{maxAttempts => 3},
        idempotencyKey => <<"chat-001">>
      }).

SSE

ok = eWCli:postStream(
      <<"https://api.openai.com/v1/chat/completions">>,
      [],
      JsonBody,   %% 须含 "stream": true
      #{
        bearer => ApiKey,
        json => true,
        retry => #{maxAttempts => 3},
        handler => fun
            ({sse, {data, <<"[DONE]"/utf8>>}}) -> continue;
            ({sse, {data, Bin}}) -> handleDelta(Bin), continue;
            ({sse, done}) -> continue;
            (done) -> continue;
            (_) -> continue
        end
      }).

Handler 事件:

事件 含义
{headers, Status, Hs, Ver, Reason} 响应头
{sse, {data, Bin}} SSE data:(OpenAI)
{sse, {event, Type, Bin}} 具名 SSE(Anthropic / Gemini)
{sse, done} data: [DONE]
{ndjson, Line} NDJSON 一行
{chunk, Bin} format => raw 的原始块
{trailers, Hs} trailer
done 响应结束

postStream 常见返回值:

结果 含义
ok 流正常结束(2xx)
{ok, #wcResp{status = 429 | 5xx}} 收到可重试状态;配置了 retry 时内部会再试
{error, {streamStarted, Reason}} 已出 2xx body 后失败,不会再重试
{error, cancelled} handler 返回了 stop
{error, timeout} 总超时或空闲超时
{error, bodyTooLarge} 超过 maxBody(流式默认无上限)

Ollama(NDJSON)

eWCli:post(<<"http://127.0.0.1:11434/api/chat">>, [], JsonBody,
           #{json => true}).

eWCli:postStream(<<"http://127.0.0.1:11434/api/chat">>, [], JsonBody,
    #{json => true, format => ndjson,
      handler => fun
          ({ndjson, Line}) -> handleLine(Line), continue;
          (done) -> continue;
          (_) -> continue
      end}).

本地默认走 NO_PROXY 里的 localhost,不经过系统代理。

附件

postMultipart 的 part:

形式 说明
{field, Name, Value} 普通字段
{file, Name, Filename, ContentType, Data} 内存中的文件
{filePath, Name, Path} 读盘;文件名取 basename,类型 application/octet-stream
{filePath, Name, Filename, ContentType, Path} 读盘,指定文件名和 MIME

filePath 时按块上传,不把整个文件读进内存。

eWCli:postMultipart(Url, [
    {field, <<"model">>, <<"whisper-1">>},
    {filePath, <<"file">>, <<"a.wav">>, <<"audio/wav">>, <<"/path/a.wav">>}
], #{bearer => Key}).

eWCli:streamToFile(TtsUrl, <<"/tmp/out.mp3">>, #{bearer => Key}).
%% 等价:sink => {file, Path},或 {fd, IoDevice}

Azure

eWCli:post(AzureUrl, [], Body, #{azureApiKey => <<"your-key">>, json => true}).

常用选项

通用

选项 默认 说明
connectTimeout 15000 建连(ms)
recvTimeout 30000;postStreaminfinity 接收总超时
maxBody 64MB;流式 / streamToFileinfinity 攒 body 上限
maxRedirects 5 0 禁用
usePool true 连接池
maxConnections 10 每目标最多连接
protocol auto auto(仅 TCP)| auto3(自动升 H3+熔断)| http3(强制)
autoDecompress true gzip / deflate / br
verify OTP 默认(校验证书) 测试自签用 verify_none

大模型 / 流式

选项 说明
bearer Authorization: Bearer
azureApiKey api-key
apiKey 二进制 → api-key;或 {Header, Value}
idempotencyKey Idempotency-Key
json => true Content-Type: application/json(已有则不覆盖)
format autopostStream 默认,按 Content-Type)/ sse / ndjson / raw
sse => true 旧开关;与 format 同时出现时 format 优先
withBody 是否把 chunk 拼进 #wcResp.body;流式默认 false
sink {file, Path}{fd, Fd}
firstByteTimeout 等响应头(ms),postStream 默认 300000
chunkIdleTimeout 相邻 chunk 空闲(ms),默认 300000
retry #{maxAttempts => N, retryBaseMs => 500, retryMaxMs => 30000},尊重 Retry-After;POST/PATCH 默认只重试确定未发送成功的连接错误,状态码重试需显式 retryOnStatus
handler 流式回调

网络

选项 说明
proxy #{host, port, user?, pass?}
HTTP_PROXY / HTTPS_PROXY 未设 proxy 时读取
noProxy / NO_PROXY 未设时默认含 localhost
socks5 / socks4 SOCKS
sslOpts / cacerts / cert / key TLS
pinnedCerts 证书 pinning
sni SNI

模块

模块 职责
eWCli 公共门面
wcConn / wcH3Conn H1+H2 / H3 连接进程
wcPool / wcH3Pool 连接池
wcStream 流式接收、解压、SSE/NDJSON、sink
wcSse / wcNdjson 行解析
wcRetry 429/5xx 与瞬时错误退避
wcAuth Basic / Bearer / api-key / Digest
wcHttp1 / wcHttp2 / wcHttp3 编解码
wcWs WebSocket
wcProxy / wcSocks 代理
wcCodec 内容编码
wcCookie / wcMultipart Cookie、multipart

测试

test/TESTING.md

E2E / 压测打本机 Caddy不连外网。仓库不提交 caddy.exe:从 Releases 下载后放到 test/caddy_tool/说明)。

rebar3 eunit
cmd /c "set EWCLI_SKIP_E2E=1&& rebar3 eunit"
cmd /c "set EWCLI_BENCH=1&& set EWCLI_SKIP_E2E=1&& rebar3 eunit"
test\run-bench.cmd

PowerShell 的 $env:EWCLI_BENCH=1 经常传不进 rebar3.cmd,压测请用 cmd /crun-bench.cmd

开发规范

开发规范.mdsrc/wsCli/ 模块以 wc 为前缀。

HPACK 表等生成脚本在 tools/,需要提交。

License

Apache-2.0

About

http/1 http/2 http/3 websocket 客户端 主要为Llm模型请求而已

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages