為什麼 SSE 重切器必須先改再測 proxy?
原本的 SSE splitter(feed 方法)吐事件時會丟掉邊界字節——
它知道 \n\n 是一個 SSE 事件邊界,但吐出去的 frame 不帶這個邊界,
只有事件內容。用它重組回程的 bytes 就會不一致。
新 API split_frames 吐「含原始邊界的 frame」。
不變量可以直接測:concat(frames) + take_remaining() == 所有輸入。
任何一個 byte 都不能少。重構成功的標準是舊的 4 個測試一個不動照過,
4 個新測試也全綠——只有這樣才算「安全重構」而不是「換一種寫法猜看看」。
整合測試:問引擎本人,不要預設期望值
Python 版的 proxy 測試用 httpx.MockTransport 假裝網路,
全程在 process 內跑。Rust 版的策略不同:
mock axum server 綁 127.0.0.1:0 隨機埠、
proxy 把請求轉發到這個本地 server、server 攔下實際收到的 method / uri / headers / body。
走真正的 TCP 網路棧,streaming 路徑才測得到,rechunk_sse 不是在內存裡跑的。
期望值的設定方式:
直接呼叫 process_request(MESSY) 問引擎本人「你會把這個輸入變成什麼」,
proxy 測試用那個答案當 expected。proxy 測試不該有引擎邏輯的意見,
只測「proxy 有沒有把引擎說的話完整送達」。
七個整合測試涵蓋:全路徑 fallback 轉發、POST body 過 pipeline、
hop-by-hop headers 過濾(connection / transfer-encoding 等)、
回程 stream 不攢整包、SSE 且無 content-encoding 才重切。
四個 Rust 特有地雷
地雷一:MutexGuard 不能跨 await。
CcrStore 的鎖必須在 await 點之前釋放,否則編譯器會說
MutexGuard 不是 Send,整個 future 就不能跨線程。
解法是用大括號把鎖的存活範圍縮小到 await 之前。
這不是 workaround,是正確設計:pipeline 是同步呼叫,鎖確實在 await 前就該釋放。
地雷二:reqwest bytes_stream 不保證 Unpin。
unfold 的 async 閉包要 .next() 需要流是 Unpin,
reqwest stream 不是。解法是 Box::pin,因為
Pin<Box<S>> 恆 Unpin。
地雷三:reqwest 刻意 default-features=false。
不開 gzip feature 就不會自動解壓。
自動解壓後 body 和 content-encoding header 就不一致,
byte-faithful 轉發直接破功。壓縮後的流要原樣穿過代理,
不能讓 reqwest 在背後悄悄解開。
地雷四:SSE 重切要躲開有 content-encoding 的流。
如果上游回應帶 content-encoding: gzip,
事件邊界(\n\n / \r\n\r\n)在密文裡找不到,
splitter 會緩衝到天荒地老。這種流必須原樣轉發,不能進 rechunk 路徑。
判斷條件:SSE(content-type: text/event-stream)
且無 content-encoding,才重切。
串流結束前要沖洗殘料
上游最後一包 SSE 資料如果沒帶結尾邊界,
splitter 的緩衝區裡還有剩餘 bytes 沒吐出去。
stream 結束(None)時要呼叫 take_remaining() 把殘料補吐,
否則最後幾個 byte 就消失了。
這也是 M7 第一步先把 SSE API 改掉的原因——
舊版沒有 take_remaining,強行用舊版 proxy 是不可能 byte-faithful 的。
數字與下一步
M7 完成後:Python 30 / Rust 40(新增 SSE 3 + proxy 7)/ parity gate 4 fixtures,
clippy 0 warnings,全綠。
commit bf024e4(feat)+ 54aad4a(docs),已推上 fork。
M7 解鎖了真流量實測——現在可以把 proxy 指向真實的 Anthropic API,
用真實的 claude-haiku 請求驗證快取命中率和壓縮省錢的實際數字。
關鍵教訓
SSE 重組的不變量要可測:concat(frames) + take_remaining() == 原始輸入,任何一個 byte 都不能少。先測這個不變量,proxy 才能放心用這個 API。
整合測試問引擎本人當期望值:proxy 測試不該寫死「引擎應該輸出 X」,直接呼叫引擎問它。引擎邏輯改了,proxy 測試自動跟上,不會變成脆測試。
reqwest default-features=false 是有意識的選擇:gzip 自動解壓是 convenience,但在 byte-faithful proxy 裡是破壞者。選技術棧時要知道每個 feature flag 在做什麼。
Rust async 的 MutexGuard 存活範圍必須主動縮小:不是 workaround,是正確設計的表達——讓鎖的存活時間精確反映實際需求。
byte-faithful 代理的核心約束傳染給所有決策:自動解壓不行、SSE 重切要躲壓縮流、殘料要沖洗——每個決策都是同一個原則的衍生。