composite_platform.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package handler
  2. import (
  3. "net/http"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/service"
  6. "github.com/gin-gonic/gin"
  7. "github.com/tidwall/gjson"
  8. )
  9. func ensureCompositeTargetPlatform(c *gin.Context, apiKey *service.APIKey, model string) {
  10. if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
  11. return
  12. }
  13. if _, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
  14. return
  15. }
  16. if platform, ok := service.DetectModelPlatform(model); ok {
  17. c.Request = c.Request.WithContext(service.WithResolvedTargetPlatform(c.Request.Context(), platform))
  18. }
  19. }
  20. func compositeTargetPlatformAllowed(c *gin.Context, apiKey *service.APIKey, model string, allowed ...string) bool {
  21. if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
  22. return true
  23. }
  24. ensureCompositeTargetPlatform(c, apiKey, model)
  25. platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
  26. if !ok {
  27. return false
  28. }
  29. for _, allowedPlatform := range allowed {
  30. if platform == allowedPlatform {
  31. return true
  32. }
  33. }
  34. return false
  35. }
  36. func compositeTargetPlatformResolved(c *gin.Context, apiKey *service.APIKey, model string) bool {
  37. if c == nil || c.Request == nil || apiKey == nil || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformComposite {
  38. return true
  39. }
  40. ensureCompositeTargetPlatform(c, apiKey, model)
  41. _, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context())
  42. return ok
  43. }
  44. func effectiveAPIKeyPlatform(c *gin.Context, apiKey *service.APIKey) string {
  45. if c != nil && c.Request != nil {
  46. if platform, ok := service.ResolvedTargetPlatformFromContext(c.Request.Context()); ok {
  47. return platform
  48. }
  49. }
  50. if apiKey == nil || apiKey.Group == nil {
  51. return ""
  52. }
  53. return apiKey.Group.Platform
  54. }
  55. func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, string, bool) {
  56. if apiKey == nil || apiKey.Group == nil {
  57. return "", nil, "", false
  58. }
  59. if apiKey.Group.Platform != service.PlatformAnthropic && apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
  60. return "", nil, "", false
  61. }
  62. effectivePlatform := effectiveAPIKeyPlatform(c, apiKey)
  63. if effectivePlatform != service.PlatformAnthropic && effectivePlatform != service.PlatformOpenAI {
  64. return "", nil, "", false
  65. }
  66. maxEffort, mappings := apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings
  67. if effectivePlatform == service.PlatformAnthropic {
  68. maxEffort, mappings = anthropicCompatibleReasoningEffortPolicy(maxEffort, mappings)
  69. }
  70. return maxEffort, mappings, apiKey.Group.MaxReasoningEffortOverLimit, true
  71. }
  72. func anthropicReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, string, bool) {
  73. if apiKey == nil || apiKey.Group == nil {
  74. return "", nil, "", false
  75. }
  76. if apiKey.Group.Platform != service.PlatformAnthropic && apiKey.Group.Platform != service.PlatformComposite {
  77. return "", nil, "", false
  78. }
  79. if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformAnthropic {
  80. return "", nil, "", false
  81. }
  82. maxEffort, mappings := anthropicCompatibleReasoningEffortPolicy(apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings)
  83. return maxEffort, mappings, apiKey.Group.MaxReasoningEffortOverLimit, true
  84. }
  85. func anthropicCompatibleReasoningEffortPolicy(maxEffort string, mappings []service.ReasoningEffortMapping) (string, []service.ReasoningEffortMapping) {
  86. if service.NormalizeMaxReasoningEffort(maxEffort) == "minimal" {
  87. maxEffort = "low"
  88. }
  89. normalizedMappings := append([]service.ReasoningEffortMapping(nil), mappings...)
  90. for i := range normalizedMappings {
  91. if service.NormalizeMaxReasoningEffort(normalizedMappings[i].To) == "minimal" {
  92. normalizedMappings[i].To = "low"
  93. }
  94. }
  95. return maxEffort, normalizedMappings
  96. }
  97. func bindRequestedReasoningEffort(c *gin.Context, body []byte, model string) {
  98. if c == nil || c.Request == nil {
  99. return
  100. }
  101. effort := service.CanonicalRequestedReasoningEffort(body, model)
  102. if effort == nil {
  103. return
  104. }
  105. c.Request = c.Request.WithContext(service.WithRequestedReasoningEffort(c.Request.Context(), *effort))
  106. }
  107. func stampOpenAIRequestedReasoningEffort(result *service.OpenAIForwardResult, c *gin.Context) {
  108. if result == nil || result.RequestedReasoningEffort != nil {
  109. return
  110. }
  111. if c == nil || c.Request == nil {
  112. return
  113. }
  114. result.RequestedReasoningEffort = service.RequestedReasoningEffortFromContext(c.Request.Context())
  115. }
  116. func stampForwardRequestedReasoningEffort(result *service.ForwardResult, requested *string) {
  117. if result == nil || result.RequestedReasoningEffort != nil {
  118. return
  119. }
  120. result.RequestedReasoningEffort = requested
  121. }
  122. func applyOpenAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool, error) {
  123. bindRequestedReasoningEffort(c, body, strings.TrimSpace(gjson.GetBytes(body, "model").String()))
  124. maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
  125. if !ok {
  126. return body, false, nil
  127. }
  128. return service.ApplyOpenAIReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
  129. }
  130. func respondOpenAIReasoningEffortPolicyError(c *gin.Context, err error, write func(*gin.Context, int, string, string)) {
  131. if c == nil || err == nil || write == nil {
  132. return
  133. }
  134. service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalPolicyDenied)
  135. write(c, http.StatusForbidden, "permission_error", err.Error())
  136. }
  137. func applyAnthropicReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool, error) {
  138. maxEffort, mappings, overLimit, ok := anthropicReasoningEffortPolicyForRequest(c, apiKey)
  139. if !ok {
  140. return body, false, nil
  141. }
  142. return service.ApplyReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
  143. }
  144. func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *service.APIKey, body []byte) {
  145. if c == nil || c.Request == nil {
  146. return
  147. }
  148. bindRequestedReasoningEffort(c, body, strings.TrimSpace(gjson.GetBytes(body, "model").String()))
  149. // The Messages bridge synthesizes a default OpenAI effort when
  150. // output_config.effort is omitted. Bind the group policy only for an
  151. // explicit client value so the ceiling does not alter that default.
  152. effort := gjson.GetBytes(body, "output_config.effort")
  153. if !effort.Exists() || effort.Type != gjson.String || strings.TrimSpace(effort.String()) == "" {
  154. return
  155. }
  156. maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
  157. if !ok {
  158. return
  159. }
  160. c.Request = c.Request.WithContext(service.WithOpenAIReasoningEffortPolicy(c.Request.Context(), maxEffort, mappings, overLimit))
  161. }