Files
sub2api/backend/internal/handler/composite_platform.go
T
刘佳林 aa7a811e66 feat(groups): add deny-or-downgrade control for reasoning effort ceiling
OpenAI/Composite groups can now reject requests that exceed the max
reasoning effort instead of always rewriting them down to the ceiling.
Existing groups keep the previous auto-downgrade behavior.
2026-09-01 10:15:45 +08:00

139 lines
4.9 KiB
Go

package handler
import (
"net/http"
"strings"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
)
func ensureCompositeTargetPlatform(c *gin.Context, apiKey *service.APIKey, model string) {
if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
return
}
if _, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
return
}
if platform, ok := service.DetectModelPlatform(model); ok {
c.Request = c.Request.WithContext(service.WithResolvedTargetPlatform(c.Request.Context(), platform))
}
}
func compositeTargetPlatformAllowed(c *gin.Context, apiKey *service.APIKey, model string, allowed ...string) bool {
if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
return true
}
ensureCompositeTargetPlatform(c, apiKey, model)
platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
if !ok {
return false
}
for _, allowedPlatform := range allowed {
if platform == allowedPlatform {
return true
}
}
return false
}
func compositeTargetPlatformResolved(c *gin.Context, apiKey *service.APIKey, model string) bool {
if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
return true
}
ensureCompositeTargetPlatform(c, apiKey, model)
_, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
return ok
}
func effectiveAPIKeyPlatform(c *gin.Context, apiKey *service.APIKey) string {
if c != nil && c.Request != nil {
if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
return platform
}
}
if apiKey == nil || apiKey.Group == nil {
return ""
}
return apiKey.Group.Platform
}
func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, string, bool) {
if apiKey == nil || apiKey.Group == nil {
return "", nil, "", false
}
if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
return "", nil, "", false
}
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformOpenAI {
return "", nil, "", false
}
return apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings, apiKey.Group.MaxReasoningEffortOverLimit, true
}
func bindRequestedReasoningEffort(c *gin.Context, body []byte, model string) {
if c == nil || c.Request == nil {
return
}
effort := service.CanonicalRequestedReasoningEffort(body, model)
if effort == nil {
return
}
c.Request = c.Request.WithContext(service.WithRequestedReasoningEffort(c.Request.Context(), *effort))
}
func stampOpenAIRequestedReasoningEffort(result *service.OpenAIForwardResult, c *gin.Context) {
if result == nil || result.RequestedReasoningEffort != nil {
return
}
if c == nil || c.Request == nil {
return
}
result.RequestedReasoningEffort = service.RequestedReasoningEffortFromContext(c.Request.Context())
}
func stampForwardRequestedReasoningEffort(result *service.ForwardResult, requested *string) {
if result == nil || result.RequestedReasoningEffort != nil {
return
}
result.RequestedReasoningEffort = requested
}
func applyOpenAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool, error) {
bindRequestedReasoningEffort(c, body, strings.TrimSpace(gjson.GetBytes(body, "model").String()))
maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
if !ok {
return body, false, nil
}
return service.ApplyOpenAIReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
}
func respondOpenAIReasoningEffortPolicyError(c *gin.Context, err error, write func(*gin.Context, int, string, string)) {
if c == nil || err == nil || write == nil {
return
}
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalPolicyDenied)
write(c, http.StatusForbidden, "permission_error", err.Error())
}
func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *service.APIKey, body []byte) {
if c == nil || c.Request == nil {
return
}
bindRequestedReasoningEffort(c, body, strings.TrimSpace(gjson.GetBytes(body, "model").String()))
// The Messages bridge synthesizes a default OpenAI effort when
// output_config.effort is omitted. Bind the group policy only for an
// explicit client value so the ceiling does not alter that default.
effort := gjson.GetBytes(body, "output_config.effort")
if !effort.Exists() || effort.Type != gjson.String || strings.TrimSpace(effort.String()) == "" {
return
}
maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
if !ok {
return
}
c.Request = c.Request.WithContext(service.WithOpenAIReasoningEffortPolicy(c.Request.Context(), maxEffort, mappings, overLimit))
}