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 }