Files
CLIProxyAPI/internal/translator/common/bytes.go
Luis Pater c1cb0c5de1 fix(translator): preserve html characters in tool arguments and fix devin sequential tool calls
- Introduce `SetStringWithoutHTMLEscape` to prevent escaping of HTML characters (`<`, `>`, `&`) in tool call arguments and argument deltas across translators.
- Handle sequential tool calls in Devin executor that share the same stream index but have distinct tool call IDs.
2026-09-14 11:04:08 +08:00

126 lines
3.4 KiB
Go

package common
import (
"bytes"
"encoding/json"
"strconv"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
func GeminiTokenCountJSON(count int64) []byte {
out := make([]byte, 0, 96)
out = append(out, `{"totalTokens":`...)
out = strconv.AppendInt(out, count, 10)
out = append(out, `,"promptTokensDetails":[{"modality":"TEXT","tokenCount":`...)
out = strconv.AppendInt(out, count, 10)
out = append(out, `}]}`...)
return out
}
func ClaudeInputTokensJSON(count int64) []byte {
out := make([]byte, 0, 32)
out = append(out, `{"input_tokens":`...)
out = strconv.AppendInt(out, count, 10)
out = append(out, '}')
return out
}
// NewRawArrayItems creates a raw item slice sized for the expected input.
func NewRawArrayItems(capacity int64) [][]byte {
if capacity <= 0 {
return nil
}
return make([][]byte, 0, int(capacity))
}
func JoinRawArray(items [][]byte) []byte {
if len(items) == 0 {
return []byte("[]")
}
size := len(items) + 1
for _, item := range items {
size += len(item)
}
out := make([]byte, 0, size)
out = append(out, '[')
for i, item := range items {
if i > 0 {
out = append(out, ',')
}
out = append(out, item...)
}
return append(out, ']')
}
// SetRawArrayItems replaces an empty JSON array at path with raw items.
// The single-item path avoids allocating an intermediate joined array.
func SetRawArrayItems(data []byte, path string, items [][]byte) []byte {
if len(items) == 0 {
return data
}
if len(items) == 1 {
array := gjson.GetBytes(data, path)
if array.Raw == "[]" && array.Index >= 0 && array.Index+len(array.Raw) <= len(data) {
out := make([]byte, 0, len(data)+len(items[0]))
out = append(out, data[:array.Index]...)
out = append(out, '[')
out = append(out, items[0]...)
out = append(out, ']')
return append(out, data[array.Index+len(array.Raw):]...)
}
}
data, _ = sjson.SetRawBytes(data, path, JoinRawArray(items))
return data
}
func SSEEventData(event string, payload []byte) []byte {
out := make([]byte, 0, len(event)+len(payload)+14)
out = append(out, "event: "...)
out = append(out, event...)
out = append(out, '\n')
out = append(out, "data: "...)
out = append(out, payload...)
return out
}
func AppendSSEEventString(out []byte, event, payload string, trailingNewlines int) []byte {
out = append(out, "event: "...)
out = append(out, event...)
out = append(out, '\n')
out = append(out, "data: "...)
out = append(out, payload...)
for i := 0; i < trailingNewlines; i++ {
out = append(out, '\n')
}
return out
}
func AppendSSEEventBytes(out []byte, event string, payload []byte, trailingNewlines int) []byte {
out = append(out, "event: "...)
out = append(out, event...)
out = append(out, '\n')
out = append(out, "data: "...)
out = append(out, payload...)
for i := 0; i < trailingNewlines; i++ {
out = append(out, '\n')
}
return out
}
// SetStringWithoutHTMLEscape sets a string field in a JSON payload without escaping
// HTML characters (<, >, &). Standard sjson.Set/SetBytes delegates string values
// containing quotes or special characters to encoding/json.Marshal, which unconditionally
// escapes <, >, & to \u003c, \u003e, \u0026.
func SetStringWithoutHTMLEscape(data []byte, path, value string) ([]byte, error) {
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
if errEncode := enc.Encode(value); errEncode != nil {
return sjson.SetBytes(data, path, value)
}
raw := bytes.TrimRight(buf.Bytes(), "\n")
return sjson.SetRawBytes(data, path, raw)
}