From 2cd686a669716f4d469df9cd3718c0e9a1a046b1 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Tue, 29 Sep 2026 12:26:15 +0100 Subject: [PATCH] perf(stream): append text deltas as JSON without an intermediate Marshal --- translate_anthropic.go | 66 +++++++++++++++++++++++++++++++++---- translate_anthropic_test.go | 17 ++++++++++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/translate_anthropic.go b/translate_anthropic.go index 4935b9d..672ca73 100644 --- a/translate_anthropic.go +++ b/translate_anthropic.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" "strings" + "unicode/utf8" ) // anthropicAPIVersion is the "anthropic-version" header value every request @@ -776,16 +777,69 @@ func (c *contentChunker) set(id, model string, created int64) { } func (c *contentChunker) build(text string) []byte { - tb, err := json.Marshal(text) - if err != nil { - panic(fmt.Sprintf("llmgateway: stream chunk failed to marshal text: %v", err)) - } - out := make([]byte, 0, len(contentChunkHead)+len(tb)+len(c.tail)) + out := make([]byte, 0, len(contentChunkHead)+len(text)+len(text)/8+2+len(c.tail)) out = append(out, contentChunkHead...) - out = append(out, tb...) + out = appendJSONString(out, text) return append(out, c.tail...) } +const jsonHex = "0123456789abcdef" + +// appendJSONString appends s as a quoted JSON string using +// encoding/json's default escaping (HTML characters, U+2028/U+2029 and +// invalid UTF-8 included), so its output equals json.Marshal(s). +func appendJSONString(dst []byte, s string) []byte { + dst = append(dst, '"') + start := 0 + for i := 0; i < len(s); { + b := s[i] + if b < utf8.RuneSelf { + if b >= 0x20 && b != '"' && b != '\\' && b != '<' && b != '>' && b != '&' { + i++ + continue + } + dst = append(dst, s[start:i]...) + switch b { + case '"', '\\': + dst = append(dst, '\\', b) + case '\b': + dst = append(dst, '\\', 'b') + case '\f': + dst = append(dst, '\\', 'f') + case '\n': + dst = append(dst, '\\', 'n') + case '\r': + dst = append(dst, '\\', 'r') + case '\t': + dst = append(dst, '\\', 't') + default: + dst = append(dst, '\\', 'u', '0', '0', jsonHex[b>>4], jsonHex[b&0xF]) + } + i++ + start = i + continue + } + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError && size == 1 { + dst = append(dst, s[start:i]...) + dst = append(dst, "\xef\xbf\xbd"...) + i += size + start = i + continue + } + if r == '\u2028' || r == '\u2029' { + dst = append(dst, s[start:i]...) + dst = append(dst, '\\', 'u', '2', '0', '2', jsonHex[r&0xF]) + i += size + start = i + continue + } + i += size + } + dst = append(dst, s[start:]...) + return append(dst, '"') +} + // usageChunk builds the final usage-only "chat.completion.chunk" payload — // empty "choices" plus a populated "usage" — mirroring the shape // provider_openai.go's forwardStream relays from a native OpenAI upstream diff --git a/translate_anthropic_test.go b/translate_anthropic_test.go index d630812..757b112 100644 --- a/translate_anthropic_test.go +++ b/translate_anthropic_test.go @@ -1150,3 +1150,20 @@ func TestContentChunkerMatchesChunk(t *testing.T) { } } } + +func TestAppendJSONStringMatchesMarshal(t *testing.T) { + var cases []string + for r := rune(0); r < 0x250; r++ { + cases = append(cases, string(r), "a"+string(r)+"b") + } + cases = append(cases, "\u2028", "\u2029", "x\u2028y\u2029z", "bad\xffutf8", "\xc3", "\xe2\x80", "\U0001F600 emoji", "", "") + for _, s := range cases { + want, err := json.Marshal(s) + if err != nil { + t.Fatalf("Marshal(%q): %v", s, err) + } + if got := appendJSONString(nil, s); !bytes.Equal(got, want) { + t.Errorf("appendJSONString(%q) = %s, want %s", s, got, want) + } + } +}