Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
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
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
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
maxReasoningEffortMappings = 64
|
||||
maxReasoningEffortValueLen = 64
|
||||
)
|
||||
|
||||
var openAIReasoningEffortValues = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
|
||||
|
||||
type openAIReasoningEffortPolicyContextKey struct{}
|
||||
|
||||
type openAIReasoningEffortPolicy struct {
|
||||
maxEffort string
|
||||
mappings []ReasoningEffortMapping
|
||||
}
|
||||
|
||||
// NormalizeMaxReasoningEffort validates and canonicalizes a group policy value.
|
||||
// Empty means that the group does not impose a ceiling.
|
||||
func NormalizeMaxReasoningEffort(raw string) string {
|
||||
value := strings.ToLower(strings.TrimSpace(raw))
|
||||
value = strings.NewReplacer("-", "", "_", "", " ", "").Replace(value)
|
||||
switch value {
|
||||
case "":
|
||||
return ""
|
||||
case "minimal":
|
||||
return "minimal"
|
||||
case "low":
|
||||
return "low"
|
||||
case "medium":
|
||||
return "medium"
|
||||
case "high":
|
||||
return "high"
|
||||
case "xhigh", "extrahigh":
|
||||
return "xhigh"
|
||||
case "max":
|
||||
return "max"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func reasoningEffortValuesForPlatform(platform string) []string {
|
||||
if platform != PlatformOpenAI && platform != PlatformComposite {
|
||||
return nil
|
||||
}
|
||||
return openAIReasoningEffortValues
|
||||
}
|
||||
|
||||
func normalizeMaxReasoningEffortForPlatform(platform, raw string) (string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
allowedValues := reasoningEffortValuesForPlatform(platform)
|
||||
if len(allowedValues) == 0 {
|
||||
return "", fmt.Errorf(
|
||||
"reasoning effort policy is only supported for platforms %q and %q",
|
||||
PlatformOpenAI,
|
||||
PlatformComposite,
|
||||
)
|
||||
}
|
||||
|
||||
value := NormalizeMaxReasoningEffort(raw)
|
||||
for _, allowed := range allowedValues {
|
||||
if value == allowed {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf(
|
||||
"reasoning effort %q is not supported for platform %q; allowed values: %s",
|
||||
raw,
|
||||
platform,
|
||||
strings.Join(allowedValues, ", "),
|
||||
)
|
||||
}
|
||||
|
||||
func reasoningEffortRank(raw string) (int, bool) {
|
||||
switch NormalizeMaxReasoningEffort(raw) {
|
||||
case "minimal":
|
||||
return 1, true
|
||||
case "low":
|
||||
return 2, true
|
||||
case "medium":
|
||||
return 3, true
|
||||
case "high":
|
||||
return 4, true
|
||||
case "xhigh":
|
||||
return 5, true
|
||||
case "max":
|
||||
return 6, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeReasoningEffortMappings validates group mapping rules against the
|
||||
// fixed effort values supported by OpenAI routes.
|
||||
func NormalizeReasoningEffortMappings(platform string, raw []ReasoningEffortMapping) ([]ReasoningEffortMapping, error) {
|
||||
if len(raw) > maxReasoningEffortMappings {
|
||||
return nil, fmt.Errorf("reasoning effort mappings cannot exceed %d entries", maxReasoningEffortMappings)
|
||||
}
|
||||
|
||||
normalized := make([]ReasoningEffortMapping, 0, len(raw))
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
for i, mapping := range raw {
|
||||
from := NormalizeMaxReasoningEffort(mapping.From)
|
||||
to := NormalizeMaxReasoningEffort(mapping.To)
|
||||
if from == "" || to == "" {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d contains an empty or unknown value", i+1)
|
||||
}
|
||||
if len(from) > maxReasoningEffortValueLen || len(to) > maxReasoningEffortValueLen {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d values cannot exceed %d characters", i+1, maxReasoningEffortValueLen)
|
||||
}
|
||||
if _, err := normalizeMaxReasoningEffortForPlatform(platform, from); err != nil {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d source: %w", i+1, err)
|
||||
}
|
||||
if _, err := normalizeMaxReasoningEffortForPlatform(platform, to); err != nil {
|
||||
return nil, fmt.Errorf("reasoning effort mapping %d target: %w", i+1, err)
|
||||
}
|
||||
key := from
|
||||
if _, exists := seen[key]; exists {
|
||||
return nil, fmt.Errorf("duplicate reasoning effort mapping source %q", from)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
normalized = append(normalized, ReasoningEffortMapping{From: from, To: to})
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// WithOpenAIReasoningEffortPolicy binds a group policy to a request after its
|
||||
// concrete target platform has been resolved to OpenAI. The policy is copied so
|
||||
// retries and asynchronous forwarding cannot observe later slice mutations.
|
||||
func WithOpenAIReasoningEffortPolicy(ctx context.Context, maxEffort string, mappings []ReasoningEffortMapping) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
policy := openAIReasoningEffortPolicy{
|
||||
maxEffort: maxEffort,
|
||||
mappings: append([]ReasoningEffortMapping(nil), mappings...),
|
||||
}
|
||||
return context.WithValue(ctx, openAIReasoningEffortPolicyContextKey{}, policy)
|
||||
}
|
||||
|
||||
// ApplyOpenAIReasoningEffortPolicyFromContext applies a policy previously bound
|
||||
// to the request. An unbound request is returned byte-for-byte unchanged.
|
||||
func ApplyOpenAIReasoningEffortPolicyFromContext(ctx context.Context, body []byte) ([]byte, bool) {
|
||||
if ctx == nil {
|
||||
return body, false
|
||||
}
|
||||
policy, ok := ctx.Value(openAIReasoningEffortPolicyContextKey{}).(openAIReasoningEffortPolicy)
|
||||
if !ok {
|
||||
return body, false
|
||||
}
|
||||
return ApplyOpenAIReasoningEffortPolicy(body, policy.maxEffort, policy.mappings)
|
||||
}
|
||||
|
||||
func mapReasoningEffort(raw string, mappings []ReasoningEffortMapping) (string, bool) {
|
||||
value := strings.TrimSpace(raw)
|
||||
canonical := NormalizeMaxReasoningEffort(value)
|
||||
for _, mapping := range mappings {
|
||||
if canonical != "" && canonical == NormalizeMaxReasoningEffort(mapping.From) {
|
||||
return strings.TrimSpace(mapping.To), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
func sanitizeGroupReasoningEffortPolicy(group *Group) {
|
||||
if group == nil {
|
||||
return
|
||||
}
|
||||
maxEffort, maxErr := normalizeMaxReasoningEffortForPlatform(group.Platform, group.MaxReasoningEffort)
|
||||
mappings, mappingsErr := NormalizeReasoningEffortMappings(group.Platform, group.ReasoningEffortMappings)
|
||||
if maxErr != nil {
|
||||
maxEffort = ""
|
||||
}
|
||||
if mappingsErr != nil {
|
||||
mappings = []ReasoningEffortMapping{}
|
||||
}
|
||||
group.MaxReasoningEffort = maxEffort
|
||||
group.ReasoningEffortMappings = mappings
|
||||
}
|
||||
|
||||
// ApplyOpenAIReasoningEffortPolicy applies one exact mapping and then caps
|
||||
// known effort levels. Omitted values remain untouched so upstream defaults
|
||||
// stay in control.
|
||||
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping) ([]byte, bool) {
|
||||
maxRank, hasMax := reasoningEffortRank(maxEffort)
|
||||
if len(body) == 0 || (!hasMax && len(mappings) == 0) {
|
||||
return body, false
|
||||
}
|
||||
|
||||
result := body
|
||||
changed := false
|
||||
for _, path := range []string{"reasoning.effort", "reasoning_effort"} {
|
||||
field := gjson.GetBytes(result, path)
|
||||
if !field.Exists() || field.Type != gjson.String {
|
||||
continue
|
||||
}
|
||||
original := strings.TrimSpace(field.String())
|
||||
if original == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
effective, _ := mapReasoningEffort(original, mappings)
|
||||
if currentRank, recognized := reasoningEffortRank(effective); recognized {
|
||||
effective = NormalizeMaxReasoningEffort(effective)
|
||||
if hasMax && currentRank > maxRank {
|
||||
effective = NormalizeMaxReasoningEffort(maxEffort)
|
||||
}
|
||||
}
|
||||
if effective == original {
|
||||
continue
|
||||
}
|
||||
|
||||
updated, err := sjson.SetBytes(result, path, effective)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result = updated
|
||||
changed = true
|
||||
}
|
||||
return result, changed
|
||||
}
|
||||
Reference in New Issue
Block a user