Files
sub2api/backend/internal/service/openai_responses_rejected_field_retry.go
T
李建琦 6d655c9903
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
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

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
}