143 lines
4.5 KiB
Go
143 lines
4.5 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
func sanitizeOpenAIResponsesReasoningEncryptedContent(ctx context.Context, provider string, body []byte) []byte {
|
|
inputResult := util.GetGJSONBytesNoCopy(body, "input")
|
|
if !inputResult.Exists() || !inputResult.IsArray() {
|
|
return body
|
|
}
|
|
provider = strings.TrimSpace(provider)
|
|
if provider == "" {
|
|
provider = "openai responses upstream"
|
|
}
|
|
|
|
// Codex backend rejects store=true and does not persist items when store=false.
|
|
// A reasoning item that still carries an id without usable encrypted_content is
|
|
// treated as a store lookup and returns:
|
|
// Item with id '...' not found. Items are not persisted when `store` is set to false.
|
|
// Strip those orphan ids unless the request explicitly opts into store=true.
|
|
stripOrphanReasoningIDs := !gjson.GetBytes(body, "store").Bool()
|
|
|
|
items := inputResult.Array()
|
|
|
|
// rebuilt accumulates the edited "input" array as JSON array bytes. It
|
|
// stays nil while no item needs editing so the common case (nothing to
|
|
// sanitize) does no allocation or rebuilding. Edits are applied directly
|
|
// to each item's own raw JSON rather than re-parsing the whole body,
|
|
// keeping the cost proportional to the item being edited.
|
|
var rebuilt []byte
|
|
itemsWritten := 0
|
|
keep := func(raw string) {
|
|
if rebuilt == nil {
|
|
return
|
|
}
|
|
if itemsWritten > 0 {
|
|
rebuilt = append(rebuilt, ',')
|
|
}
|
|
rebuilt = append(rebuilt, raw...)
|
|
itemsWritten++
|
|
}
|
|
startRebuild := func(index int) {
|
|
if rebuilt != nil {
|
|
return
|
|
}
|
|
// First item that needs editing: start the buffer and backfill
|
|
// it with the raw JSON of every preceding item.
|
|
rebuilt = make([]byte, 0, len(inputResult.Raw))
|
|
rebuilt = append(rebuilt, '[')
|
|
for i := range index {
|
|
keep(items[i].Raw)
|
|
}
|
|
}
|
|
|
|
for index, item := range items {
|
|
if strings.TrimSpace(item.Get("type").String()) != "reasoning" {
|
|
keep(item.Raw)
|
|
continue
|
|
}
|
|
|
|
encryptedContent := item.Get("encrypted_content")
|
|
itemID := strings.TrimSpace(item.Get("id").String())
|
|
if itemID == "" {
|
|
itemID = fmt.Sprintf("input[%d]", index)
|
|
}
|
|
|
|
if !encryptedContent.Exists() {
|
|
if stripOrphanReasoningIDs && item.Get("id").Exists() {
|
|
nextItem, err := sjson.Delete(item.Raw, "id")
|
|
if err != nil {
|
|
helps.LogWithRequestID(ctx).Debugf("%s: failed to drop orphan reasoning id at input[%d]: %v", provider, index, err)
|
|
keep(item.Raw)
|
|
continue
|
|
}
|
|
startRebuild(index)
|
|
keep(nextItem)
|
|
helps.LogWithRequestID(ctx).Debugf("%s: dropped orphan reasoning id at input[%d] item_id=%q reason=missing encrypted_content with store disabled", provider, index, itemID)
|
|
continue
|
|
}
|
|
keep(item.Raw)
|
|
continue
|
|
}
|
|
|
|
reason := ""
|
|
switch encryptedContent.Type {
|
|
case gjson.String:
|
|
rawSignature := encryptedContent.String()
|
|
if rawSignature != strings.TrimSpace(rawSignature) {
|
|
reason = "encrypted_content has leading or trailing whitespace"
|
|
} else if _, err := signature.InspectGPTReasoningSignature(rawSignature); err != nil {
|
|
reason = err.Error()
|
|
}
|
|
case gjson.Null:
|
|
reason = "encrypted_content is null"
|
|
default:
|
|
reason = fmt.Sprintf("encrypted_content must be a string, got %s", encryptedContent.Type.String())
|
|
}
|
|
if reason == "" {
|
|
keep(item.Raw)
|
|
continue
|
|
}
|
|
|
|
nextItem, err := sjson.Delete(item.Raw, "encrypted_content")
|
|
if err != nil {
|
|
helps.LogWithRequestID(ctx).Debugf("%s: failed to drop invalid reasoning encrypted_content at input[%d]: %v", provider, index, err)
|
|
keep(item.Raw)
|
|
continue
|
|
}
|
|
if stripOrphanReasoningIDs && item.Get("id").Exists() {
|
|
if nextID, errID := sjson.Delete(nextItem, "id"); errID != nil {
|
|
helps.LogWithRequestID(ctx).Debugf("%s: failed to drop reasoning id after invalid encrypted_content at input[%d]: %v", provider, index, errID)
|
|
} else {
|
|
nextItem = nextID
|
|
}
|
|
}
|
|
|
|
startRebuild(index)
|
|
keep(nextItem)
|
|
|
|
helps.LogWithRequestID(ctx).Debugf("%s: dropped invalid reasoning encrypted_content at input[%d] item_id=%q reason=%s", provider, index, itemID, reason)
|
|
}
|
|
|
|
if rebuilt == nil {
|
|
return body
|
|
}
|
|
rebuilt = append(rebuilt, ']')
|
|
|
|
updated, err := sjson.SetRawBytes(body, "input", rebuilt)
|
|
if err != nil {
|
|
helps.LogWithRequestID(ctx).Debugf("%s: failed to rebuild input array while sanitizing reasoning encrypted_content: %v", provider, err)
|
|
return body
|
|
}
|
|
return updated
|
|
}
|