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 "[]" }