@@ -32,7 +32,11 @@ type anthropicResponse struct {
3232 InputTokens int `json:"input_tokens"`
3333 OutputTokens int `json:"output_tokens"`
3434 } `json:"usage"`
35- Model string `json:"model"`
35+ Model string `json:"model"`
36+ Content []struct {
37+ Type string `json:"type"`
38+ Text string `json:"text"`
39+ } `json:"content"`
3640}
3741
3842func anthropicHandler (s * Server ) http.Handler {
@@ -144,11 +148,18 @@ func anthropicHandler(s *Server) http.Handler {
144148 // Both upstream usage AND a local estimate are available — record
145149 // the comparison so the §8 >25% accuracy criterion is measurable.
146150 // v0.4: use EstimateCompletion (the estimator the fallback actually
147- // bills with, incl. the +100 round-up) on the completion text — the
151+ // bills with, incl. +100 round-up) on the completion text — the
148152 // harness must measure the estimator the cap uses, not EstimatePrompt
149153 // run on completion text (wrong side + no round-up = biased-low).
150- tokens .RecordSample ("anthropic" , model ,
151- tokens .EstimateCompletion (model , completionText ), outTok )
154+ // v0.5.0: guard — skip the sample when completionText is still "".
155+ // An empty completion yields EstimateCompletion(model,"")=100
156+ // (roundUp n<=0 -> step), which false-triggers the §8 >25% kill
157+ // criterion the harness was built to make evaluable. There is
158+ // nothing meaningful to compare against outTok in that case.
159+ if completionText != "" {
160+ tokens .RecordSample ("anthropic" , model ,
161+ tokens .EstimateCompletion (model , completionText ), outTok )
162+ }
152163 }
153164 usd := budget .CostFromUsageWithProvider ("anthropic" , model , inTok , outTok )
154165 if _ , err := s .led .CommitDelta (s .projectRoot , estimate , inTok , outTok , usd ); err != nil {
@@ -199,7 +210,14 @@ func parseAnthropicUsage(body []byte) (int, int, string, string) {
199210 if err := json .Unmarshal (body , & unary ); err == nil &&
200211 (unary .Usage .InputTokens > 0 || unary .Usage .OutputTokens > 0 || unary .Model != "" ) {
201212 if unary .Usage .InputTokens > 0 || unary .Usage .OutputTokens > 0 {
202- return unary .Usage .InputTokens , unary .Usage .OutputTokens , "" , unary .Model
213+ // v0.5.0: fix-accuracy-harness-empty-completion-unary — the unary
214+ // branch previously returned "" for completionText, so RecordSample
215+ // measured EstimateCompletion(model, "")=100 on every unary response
216+ // regardless of real completion length, false-triggering the §8 >25%
217+ // kill criterion. Extract the content-block text so the harness
218+ // measures a real estimate on unary traffic too.
219+ return unary .Usage .InputTokens , unary .Usage .OutputTokens ,
220+ anthropicContentText (unary .Content ), unary .Model
203221 }
204222 }
205223
@@ -248,6 +266,25 @@ func parseAnthropicUsage(body []byte) (int, int, string, string) {
248266 return inTok , outTok , text .String (), model
249267}
250268
269+ // anthropicContentText flattens the content blocks of a unary Anthropic
270+ // response into one string, so the accuracy harness can EstimateCompletion on
271+ // the real completion text instead of "" (which rounds up to 100 and
272+ // false-triggers the §8 >25% kill criterion on unary traffic). Text blocks
273+ // contribute their text; non-text blocks (tool_use, etc.) contribute nothing —
274+ // they are not billed as completion tokens by the estimator.
275+ func anthropicContentText (blocks []struct {
276+ Type string `json:"type"`
277+ Text string `json:"text"`
278+ }) string {
279+ var b strings.Builder
280+ for _ , blk := range blocks {
281+ if blk .Type == "text" || blk .Type == "" {
282+ b .WriteString (blk .Text )
283+ }
284+ }
285+ return b .String ()
286+ }
287+
251288// anthropicPromptText flattens an Anthropic Messages request body into one big
252289// string for tiktoken estimation when the upstream omits usage entirely.
253290func anthropicPromptText (body []byte ) string {
@@ -291,9 +328,27 @@ func estimatePromptTokens(body []byte) int {
291328func copyHeader (dst , src http.Header ) {
292329 for k , vv := range src {
293330 // Drop hop-by-hop and host headers; let Go re-set transport.
331+ // Accept-Encoding is also dropped: Go's http transport only
332+ // auto-decompresses a gzipped response when IT added the
333+ // Accept-Encoding header. A caller-set value (Go/Node/fetch add
334+ // "gzip" by default) is forwarded verbatim, the upstream then
335+ // gzips the unary JSON body, and the transport passes the raw
336+ // \x1f\x8b bytes through untouched — so io.ReadAll(resp.Body)
337+ // yields gzip bytes, parseAnthropicUsage/parseDeepSeekUsage
338+ // json.Unmarshal them into an error (in=0, out=0,
339+ // completionText=""), and the per-side fallback bills
340+ // promptTokens + EstimateCompletion(model, "")=100 instead of
341+ // the real usage. For a unary response with thousands of real
342+ // output tokens that under-bills by orders of magnitude, so
343+ // realized spend can exceed the cap without tripping it
344+ // (fail-open) — the exact property this product exists to
345+ // prevent. Stripping it here lets the proxy's own transport
346+ // manage gzip (adds Accept-Encoding, decompresses, strips
347+ // Content-Encoding) for all five handlers that share this
348+ // copier. v0.5.0: fix-gzip-accept-encoding-forwarded.
294349 if k == "Connection" || k == "Keep-Alive" || k == "Proxy-Connection" ||
295350 k == "Te" || k == "Trailer" || k == "Transfer-Encoding" || k == "Upgrade" ||
296- k == "Host" || k == "Content-Length" {
351+ k == "Host" || k == "Content-Length" || k == "Accept-Encoding" {
297352 continue
298353 }
299354 for _ , v := range vv {
0 commit comments