Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const maxOpenAIResponsesRejectedFieldRetries = 6
|
|
|
|
var (
|
|
openAIResponsesRejectedNamespaceParamPattern = regexp.MustCompile(`(?i)^input\[(\d+)\]\.namespace$`)
|
|
openAIResponsesRejectedMessageParamPattern = regexp.MustCompile(`(?i)(?:unknown|unsupported)[ _-]+parameter\s*(?::|=|is)?\s*["']?(max_output_tokens|input\[\d+\]\.namespace)(?:["']|\b)`)
|
|
)
|
|
|
|
type openAIResponsesRejectedFieldRetryState struct {
|
|
attempts int
|
|
seenBodyHashes map[[sha256.Size]byte]struct{}
|
|
}
|
|
|
|
func newOpenAIResponsesRejectedFieldRetryState(initialBody []byte) *openAIResponsesRejectedFieldRetryState {
|
|
state := &openAIResponsesRejectedFieldRetryState{
|
|
seenBodyHashes: make(map[[sha256.Size]byte]struct{}, maxOpenAIResponsesRejectedFieldRetries+1),
|
|
}
|
|
state.remember(initialBody)
|
|
return state
|
|
}
|
|
|
|
func (s *openAIResponsesRejectedFieldRetryState) Allow(nextBody []byte) bool {
|
|
if s == nil || len(nextBody) == 0 || s.attempts >= maxOpenAIResponsesRejectedFieldRetries {
|
|
return false
|
|
}
|
|
bodyHash := sha256.Sum256(nextBody)
|
|
if _, seen := s.seenBodyHashes[bodyHash]; seen {
|
|
return false
|
|
}
|
|
s.seenBodyHashes[bodyHash] = struct{}{}
|
|
s.attempts++
|
|
return true
|
|
}
|
|
|
|
func (s *openAIResponsesRejectedFieldRetryState) remember(body []byte) {
|
|
if s == nil || len(body) == 0 {
|
|
return
|
|
}
|
|
if s.seenBodyHashes == nil {
|
|
s.seenBodyHashes = make(map[[sha256.Size]byte]struct{}, maxOpenAIResponsesRejectedFieldRetries+1)
|
|
}
|
|
s.seenBodyHashes[sha256.Sum256(body)] = struct{}{}
|
|
}
|
|
|
|
func normalizeOpenAIResponsesRejectedFieldRetryBody(statusCode int, body, responseBody []byte) ([]byte, string, bool, error) {
|
|
if statusCode != http.StatusBadRequest || len(body) == 0 || len(responseBody) == 0 {
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
code := strings.ToLower(strings.TrimSpace(extractUpstreamErrorCode(responseBody)))
|
|
message := strings.ToLower(strings.TrimSpace(extractUpstreamErrorMessage(responseBody)))
|
|
if !isExplicitOpenAIResponsesFieldRejection(code, message) {
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
param := strings.ToLower(strings.TrimSpace(gjson.GetBytes(responseBody, "error.param").String()))
|
|
if param == "" {
|
|
param = openAIResponsesRejectedParamFromMessage(message)
|
|
}
|
|
if index, ok := openAIResponsesRejectedNamespaceIndex(param); ok {
|
|
return removeOpenAIResponsesRejectedNamespaceAtIndex(body, index)
|
|
}
|
|
if param == "max_output_tokens" && gjson.GetBytes(body, "max_output_tokens").Exists() {
|
|
retryBody, err := sjson.DeleteBytes(body, "max_output_tokens")
|
|
if err != nil {
|
|
return nil, "", false, fmt.Errorf("delete rejected max_output_tokens: %w", err)
|
|
}
|
|
return retryBody, "max_output_tokens parameter rejection", true, nil
|
|
}
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
func isExplicitOpenAIResponsesFieldRejection(code, message string) bool {
|
|
switch strings.TrimSpace(code) {
|
|
case "unknown_parameter", "unsupported_parameter":
|
|
return true
|
|
}
|
|
return strings.Contains(message, "unknown parameter") ||
|
|
strings.Contains(message, "unsupported parameter")
|
|
}
|
|
|
|
func openAIResponsesRejectedParamFromMessage(message string) string {
|
|
match := openAIResponsesRejectedMessageParamPattern.FindStringSubmatch(strings.TrimSpace(message))
|
|
if len(match) != 2 {
|
|
return ""
|
|
}
|
|
return strings.ToLower(strings.TrimSpace(match[1]))
|
|
}
|
|
|
|
func openAIResponsesRejectedNamespaceIndex(param string) (int, bool) {
|
|
match := openAIResponsesRejectedNamespaceParamPattern.FindStringSubmatch(strings.TrimSpace(param))
|
|
if len(match) != 2 {
|
|
return 0, false
|
|
}
|
|
index, err := strconv.Atoi(match[1])
|
|
if err == nil && index >= 0 {
|
|
return index, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func removeOpenAIResponsesRejectedNamespaceAtIndex(body []byte, index int) ([]byte, string, bool, error) {
|
|
itemPath := fmt.Sprintf("input.%d", index)
|
|
itemType := strings.ToLower(strings.TrimSpace(gjson.GetBytes(body, itemPath+".type").String()))
|
|
switch itemType {
|
|
case "function_call", "tool_call", "custom_tool_call", "mcp_tool_call":
|
|
default:
|
|
return nil, "", false, nil
|
|
}
|
|
|
|
namespacePath := itemPath + ".namespace"
|
|
if !gjson.GetBytes(body, namespacePath).Exists() {
|
|
return nil, "", false, nil
|
|
}
|
|
retryBody, err := sjson.DeleteBytes(body, namespacePath)
|
|
if err != nil {
|
|
return nil, "", false, fmt.Errorf("delete rejected namespace at input[%d]: %w", index, err)
|
|
}
|
|
return retryBody, "indexed namespace parameter rejection", true, nil
|
|
}
|