146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
package openai
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
func shouldHandleResponsesWebsocketPrewarmLocally(rawJSON []byte, lastRequest []byte, allowIncrementalInputWithPreviousResponseID bool) bool {
|
|
if allowIncrementalInputWithPreviousResponseID || len(lastRequest) != 0 {
|
|
return false
|
|
}
|
|
if strings.TrimSpace(gjson.GetBytes(rawJSON, "type").String()) != wsRequestTypeCreate {
|
|
return false
|
|
}
|
|
generateResult := gjson.GetBytes(rawJSON, "generate")
|
|
return generateResult.Exists() && !generateResult.Bool()
|
|
}
|
|
|
|
func writeResponsesWebsocketSyntheticPrewarm(
|
|
c *gin.Context,
|
|
writer *responsesWebsocketWriter,
|
|
requestJSON []byte,
|
|
wsTimelineLog websocketTimelineAppender,
|
|
sessionID string,
|
|
) error {
|
|
payloads, errPayloads := syntheticResponsesWebsocketPrewarmPayloads(requestJSON)
|
|
if errPayloads != nil {
|
|
return errPayloads
|
|
}
|
|
for i := 0; i < len(payloads); i++ {
|
|
markAPIResponseTimestamp(c)
|
|
// log.Infof(
|
|
// "responses websocket: downstream_out id=%s type=%d event=%s payload=%s",
|
|
// sessionID,
|
|
// websocket.TextMessage,
|
|
// websocketPayloadEventType(payloads[i]),
|
|
// websocketPayloadPreview(payloads[i]),
|
|
// )
|
|
if errWrite := writeResponsesWebsocketPayload(writer, wsTimelineLog, payloads[i], time.Now()); errWrite != nil {
|
|
log.Warnf(
|
|
"responses websocket: downstream_out write failed id=%s event=%s error=%v",
|
|
sessionID,
|
|
websocketPayloadEventType(payloads[i]),
|
|
errWrite,
|
|
)
|
|
return errWrite
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func syntheticResponsesWebsocketPrewarmPayloads(requestJSON []byte) ([][]byte, error) {
|
|
responseID := "resp_prewarm_" + uuid.NewString()
|
|
createdAt := time.Now().Unix()
|
|
modelName := strings.TrimSpace(gjson.GetBytes(requestJSON, "model").String())
|
|
|
|
createdPayload := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`)
|
|
var errSet error
|
|
createdPayload, errSet = sjson.SetBytes(createdPayload, "response.id", responseID)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
createdPayload, errSet = sjson.SetBytes(createdPayload, "response.created_at", createdAt)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
if modelName != "" {
|
|
createdPayload, errSet = sjson.SetBytes(createdPayload, "response.model", modelName)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
|
|
}
|
|
|
|
completedPayload := []byte(`{"type":"response.completed","sequence_number":1,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"output":[],"usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":0}}}`)
|
|
completedPayload, errSet = sjson.SetBytes(completedPayload, "response.id", responseID)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
completedPayload, errSet = sjson.SetBytes(completedPayload, "response.created_at", createdAt)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
if modelName != "" {
|
|
completedPayload, errSet = sjson.SetBytes(completedPayload, "response.model", modelName)
|
|
if errSet != nil {
|
|
return nil, errSet
|
|
}
|
|
}
|
|
|
|
return [][]byte{createdPayload, completedPayload}, nil
|
|
}
|
|
|
|
// inputContainsFullTranscript returns true when the input array carries compact
|
|
// replay markers that indicate the client already sent the full conversation
|
|
// transcript. Merging that input with stale lastRequest/lastResponseOutput
|
|
// would duplicate or break function_call/function_call_output pairings, so the
|
|
// caller should use the input as-is.
|
|
//
|
|
// Assistant messages alone are not enough to classify the payload as a replay:
|
|
// incremental websocket requests may legitimately append assistant items.
|
|
func inputContainsFullTranscript(input gjson.Result) bool {
|
|
if !input.IsArray() {
|
|
return false
|
|
}
|
|
for _, item := range input.Array() {
|
|
t := item.Get("type").String()
|
|
if t == "compaction" || t == "compaction_summary" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func inputWithoutCompactionItems(input gjson.Result) string {
|
|
if !input.IsArray() {
|
|
return normalizeJSONArrayRaw([]byte(input.Raw))
|
|
}
|
|
filtered := make([]string, 0, len(input.Array()))
|
|
for _, item := range input.Array() {
|
|
t := item.Get("type").String()
|
|
if t == "compaction" || t == "compaction_summary" {
|
|
continue
|
|
}
|
|
filtered = append(filtered, item.Raw)
|
|
}
|
|
return "[" + strings.Join(filtered, ",") + "]"
|
|
}
|
|
|
|
func normalizeJSONArrayRaw(raw []byte) string {
|
|
trimmed := strings.TrimSpace(string(raw))
|
|
if trimmed == "" {
|
|
return "[]"
|
|
}
|
|
result := gjson.Parse(trimmed)
|
|
if result.Type == gjson.JSON && result.IsArray() {
|
|
return trimmed
|
|
}
|
|
return "[]"
|
|
}
|