Files
2026-07-13 12:35:43 +08:00

282 lines
7.6 KiB
Go

package auth
import (
"bytes"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
var modelFieldPaths = []string{"model", "modelVersion", "response.model", "response.modelVersion", "message.model"}
const maxPendingBufSize = 1 << 20 // 1MB limit for pending buffer
func rewriteSSEPayloadLines(payload []byte, targetModel string) []byte {
if targetModel == "" || len(payload) == 0 {
return payload
}
lines := bytes.Split(payload, []byte("\n"))
out := make([][]byte, 0, len(lines))
for _, line := range lines {
prefix, jsonData, ok := extractSSEDataLine(line)
if ok && len(jsonData) > 0 && jsonData[0] == '{' && gjson.ValidBytes(jsonData) {
rewritten := rewriteModelInResponse(jsonData, targetModel)
line = append(append([]byte{}, prefix...), rewritten...)
}
out = append(out, line)
}
joined := bytes.Join(out, []byte("\n"))
if len(payload) > 0 && payload[len(payload)-1] == '\n' && (len(joined) == 0 || joined[len(joined)-1] != '\n') {
joined = append(joined, '\n')
}
return joined
}
func rewriteModelInResponse(data []byte, targetModel string) []byte {
if targetModel == "" || len(data) == 0 {
return data
}
for _, path := range modelFieldPaths {
if gjson.GetBytes(data, path).Exists() {
data, _ = sjson.SetBytes(data, path, targetModel)
log.Debugf("response rewriter: rewrote model at path %s to %s", path, targetModel)
}
}
return data
}
// StreamRewriteOptions configures the stream rewriter.
type StreamRewriteOptions struct {
RewriteModel string
}
// StreamRewriter rewrites model names in streaming SSE responses.
type StreamRewriter struct {
options StreamRewriteOptions
pendingBuf []byte
}
// NewStreamRewriter creates a new stream rewriter.
func NewStreamRewriter(options StreamRewriteOptions) *StreamRewriter {
return &StreamRewriter{
options: options,
pendingBuf: nil,
}
}
// RewriteChunk rewrites model names in a single SSE chunk.
func (r *StreamRewriter) RewriteChunk(chunk []byte) []byte {
if r.options.RewriteModel == "" {
return chunk
}
if len(r.pendingBuf) > 0 {
combined := make([]byte, 0, len(r.pendingBuf)+1+len(chunk))
combined = append(combined, r.pendingBuf...)
if combined[len(combined)-1] != '\n' {
combined = append(combined, '\n')
}
combined = append(combined, chunk...)
chunk = combined
r.pendingBuf = nil
}
chunk = normalizeGluedSSEEvents(chunk)
if len(chunk) > maxPendingBufSize {
return chunk
}
// Handle raw JSON chunks (Gemini/OpenAI format without SSE "data:" prefix)
trimmed := bytes.TrimSpace(chunk)
if len(trimmed) > 0 && trimmed[0] == '{' && gjson.ValidBytes(trimmed) {
rewritten := trimmed
if r.options.RewriteModel != "" {
rewritten = rewriteModelInResponse(rewritten, r.options.RewriteModel)
}
return rewritten
}
lastDoubleNewline := bytes.LastIndex(chunk, []byte("\n\n"))
var processChunk []byte
if lastDoubleNewline >= 0 {
afterComplete := chunk[lastDoubleNewline+2:]
if len(afterComplete) > 0 && !bytes.Equal(afterComplete, []byte("\n")) {
processChunk = chunk[:lastDoubleNewline+2]
r.pendingBuf = make([]byte, len(afterComplete))
copy(r.pendingBuf, afterComplete)
} else {
processChunk = chunk
}
} else if gjson.ValidBytes(extractLastDataPayload(chunk)) {
processChunk = chunk
} else if len(bytes.TrimSpace(chunk)) == 0 {
return chunk
} else if len(chunk) > 0 {
r.pendingBuf = make([]byte, len(chunk))
copy(r.pendingBuf, chunk)
return nil
} else {
return chunk
}
lines := bytes.Split(processChunk, []byte("\n"))
var result [][]byte
var pendingEvent []byte
skipBlanks := false
for _, line := range lines {
if len(line) == 0 && skipBlanks {
continue
}
if len(line) != 0 && skipBlanks {
skipBlanks = false
}
if bytes.HasPrefix(line, []byte("event:")) {
pendingEvent = line
continue
}
dataPrefix, jsonData, found := extractSSEDataLine(line)
if found && len(jsonData) > 0 && jsonData[0] == '{' {
if !gjson.ValidBytes(jsonData) {
if pendingEvent != nil {
r.pendingBuf = append(pendingEvent, '\n')
r.pendingBuf = append(r.pendingBuf, line...)
pendingEvent = nil
} else {
r.pendingBuf = append(r.pendingBuf, line...)
}
continue
}
if pendingEvent != nil {
result = append(result, pendingEvent)
pendingEvent = nil
}
rewritten := jsonData
if r.options.RewriteModel != "" {
rewritten = rewriteModelInResponse(jsonData, r.options.RewriteModel)
}
result = append(result, append(dataPrefix, rewritten...))
continue
}
if pendingEvent != nil {
result = append(result, pendingEvent)
pendingEvent = nil
}
result = append(result, line)
}
if pendingEvent != nil {
result = append(result, pendingEvent)
}
joined := bytes.Join(result, []byte("\n"))
if len(joined) == 0 && len(chunk) > 0 {
return rewriteSSEPayloadLines(chunk, r.options.RewriteModel)
}
return joined
}
func extractLastDataPayload(chunk []byte) []byte {
lines := bytes.Split(chunk, []byte("\n"))
for i := len(lines) - 1; i >= 0; i-- {
if _, jsonData, found := extractSSEDataLine(lines[i]); found && len(jsonData) > 0 {
return jsonData
}
}
return nil
}
func extractSSEDataLine(line []byte) (prefix []byte, jsonData []byte, ok bool) {
if jsonData, found := bytes.CutPrefix(line, []byte("data: ")); found {
return []byte("data: "), jsonData, true
}
if jsonData, found := bytes.CutPrefix(line, []byte("data:")); found {
return []byte("data:"), jsonData, true
}
return nil, nil, false
}
func normalizeGluedSSEEvents(chunk []byte) []byte {
if len(chunk) == 0 {
return chunk
}
// Antigravity/Gemini translators emit event frames without trailing blank lines.
// When multiple frames are buffered back-to-back they can glue as "...}event:...".
// Only split when the bytes before the glue close a valid SSE data JSON object.
chunk = safeReplaceGlued(chunk, []byte("}event:"), []byte("}\n\nevent:"))
chunk = safeReplaceGlued(chunk, []byte("}\r\nevent:"), []byte("}\r\n\r\nevent:"))
// Codex executor emits one "data: {json}" chunk per SSE line without trailing newlines.
// Buffered chunks can glue as "...}data:...".
chunk = safeReplaceGlued(chunk, []byte("}data:"), []byte("}\ndata:"))
chunk = safeReplaceGlued(chunk, []byte("}\r\ndata:"), []byte("}\r\ndata:"))
return chunk
}
func safeReplaceGlued(chunk []byte, old, new []byte) []byte {
if len(old) == 0 || len(chunk) == 0 {
return chunk
}
if !bytes.Contains(chunk, old) {
return chunk
}
var result []byte
remaining := chunk
for {
idx := bytes.Index(remaining, old)
if idx == -1 {
result = append(result, remaining...)
break
}
lineStart := bytes.LastIndexByte(remaining[:idx], '\n')
var part []byte
if lineStart == -1 {
part = remaining[:idx+1]
} else {
part = remaining[lineStart+1 : idx+1]
}
_, jsonData, ok := extractSSEDataLine(part)
if ok && len(jsonData) > 0 && gjson.ValidBytes(jsonData) {
result = append(result, remaining[:idx]...)
result = append(result, new...)
remaining = remaining[idx+len(old):]
continue
}
result = append(result, remaining[:idx+len(old)]...)
remaining = remaining[idx+len(old):]
}
return result
}
// Finish flushes any buffered partial SSE data at the end of a stream.
func (r *StreamRewriter) Finish() []byte {
if len(r.pendingBuf) == 0 {
return nil
}
buf := make([]byte, len(r.pendingBuf)+2)
copy(buf, r.pendingBuf)
buf[len(r.pendingBuf)] = '\n'
buf[len(r.pendingBuf)+1] = '\n'
buf = normalizeGluedSSEEvents(buf)
r.pendingBuf = nil
out := r.RewriteChunk(buf)
if len(r.pendingBuf) > 0 {
tail := rewriteSSEPayloadLines(r.pendingBuf, r.options.RewriteModel)
r.pendingBuf = nil
if len(tail) > 0 {
if len(out) > 0 {
out = append(out, tail...)
} else {
out = tail
}
}
}
return out
}