composite_platform.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
  6. )
  7. // WithResolvedTargetPlatform stores the concrete provider chosen for a request
  8. // made through a composite group.
  9. func WithResolvedTargetPlatform(ctx context.Context, platform string) context.Context {
  10. platform = strings.TrimSpace(platform)
  11. if ctx == nil || platform == "" {
  12. return ctx
  13. }
  14. return context.WithValue(ctx, ctxkey.ResolvedTargetPlatform, platform)
  15. }
  16. // ResolvedTargetPlatformFromContext returns the concrete provider chosen for
  17. // the current request, if one was resolved.
  18. func ResolvedTargetPlatformFromContext(ctx context.Context) (string, bool) {
  19. if ctx == nil {
  20. return "", false
  21. }
  22. platform, ok := ctx.Value(ctxkey.ResolvedTargetPlatform).(string)
  23. platform = strings.TrimSpace(platform)
  24. if !ok || platform == "" {
  25. return "", false
  26. }
  27. return platform, true
  28. }
  29. func WithCompositeRouteDecision(ctx context.Context, decision CompositeRouteDecision) context.Context {
  30. if ctx == nil || !decision.Matched {
  31. return ctx
  32. }
  33. ctx = WithResolvedTargetPlatform(ctx, decision.TargetPlatform)
  34. if model := strings.TrimSpace(decision.UpstreamModel); model != "" {
  35. ctx = context.WithValue(ctx, ctxkey.ResolvedUpstreamModel, model)
  36. }
  37. if model := strings.TrimSpace(decision.PublicModel); model != "" {
  38. ctx = context.WithValue(ctx, ctxkey.RequestedPublicModel, model)
  39. }
  40. if source := strings.TrimSpace(decision.Source); source != "" {
  41. ctx = context.WithValue(ctx, ctxkey.CompositeRouteSource, source)
  42. }
  43. return ctx
  44. }
  45. func ResolvedUpstreamModelFromContext(ctx context.Context) (string, bool) {
  46. if ctx == nil {
  47. return "", false
  48. }
  49. model, ok := ctx.Value(ctxkey.ResolvedUpstreamModel).(string)
  50. model = strings.TrimSpace(model)
  51. if !ok || model == "" {
  52. return "", false
  53. }
  54. return model, true
  55. }
  56. func RequestedPublicModelFromContext(ctx context.Context) (string, bool) {
  57. if ctx == nil {
  58. return "", false
  59. }
  60. model, ok := ctx.Value(ctxkey.RequestedPublicModel).(string)
  61. model = strings.TrimSpace(model)
  62. if !ok || model == "" {
  63. return "", false
  64. }
  65. return model, true
  66. }
  67. func CompositeRouteSourceFromContext(ctx context.Context) (string, bool) {
  68. if ctx == nil {
  69. return "", false
  70. }
  71. source, ok := ctx.Value(ctxkey.CompositeRouteSource).(string)
  72. source = strings.TrimSpace(source)
  73. if !ok || source == "" {
  74. return "", false
  75. }
  76. return source, true
  77. }
  78. // DetectModelPlatform maps common public model IDs to the concrete provider
  79. // platform used by sub2api. It intentionally returns false for ambiguous model
  80. // names so composite groups fail closed instead of guessing.
  81. func DetectModelPlatform(model string) (string, bool) {
  82. normalized := strings.ToLower(strings.TrimSpace(model))
  83. if normalized == "" {
  84. return "", false
  85. }
  86. normalized = strings.TrimPrefix(normalized, "models/")
  87. if slash := strings.IndexByte(normalized, '/'); slash > 0 {
  88. provider := strings.TrimSpace(normalized[:slash])
  89. rest := strings.TrimSpace(normalized[slash+1:])
  90. switch provider {
  91. case "anthropic", "claude":
  92. return PlatformAnthropic, true
  93. case "openai", "chatgpt":
  94. return PlatformOpenAI, true
  95. case "google", "google-ai-studio", "gemini":
  96. return PlatformGemini, true
  97. case "xai", "x-ai", "grok":
  98. return PlatformGrok, true
  99. case "kimi", "moonshot":
  100. return PlatformKimi, true
  101. case "zhipu", "glm", "bigmodel":
  102. return PlatformZhipu, true
  103. case "deepseek":
  104. return PlatformDeepseek, true
  105. case "minimax":
  106. return PlatformMiniMax, true
  107. }
  108. if rest != "" {
  109. normalized = strings.TrimPrefix(rest, "models/")
  110. }
  111. }
  112. switch {
  113. case strings.HasPrefix(normalized, "anthropic.claude-"),
  114. strings.HasPrefix(normalized, "claude-"):
  115. return PlatformAnthropic, true
  116. case strings.HasPrefix(normalized, "gpt-"),
  117. strings.HasPrefix(normalized, "chatgpt-"),
  118. strings.HasPrefix(normalized, "codex-"),
  119. strings.HasPrefix(normalized, "text-embedding-"),
  120. strings.HasPrefix(normalized, "text-moderation-"),
  121. strings.HasPrefix(normalized, "omni-moderation-"),
  122. strings.HasPrefix(normalized, "dall-e-"),
  123. strings.HasPrefix(normalized, "gpt-image-"),
  124. strings.HasPrefix(normalized, "tts-"),
  125. strings.HasPrefix(normalized, "whisper-"),
  126. hasOpenAISeriesPrefix(normalized):
  127. return PlatformOpenAI, true
  128. case strings.HasPrefix(normalized, "gemini-"),
  129. strings.HasPrefix(normalized, "learnlm-"):
  130. return PlatformGemini, true
  131. case normalized == "grok" || strings.HasPrefix(normalized, "grok-"):
  132. return PlatformGrok, true
  133. case normalized == "k3",
  134. normalized == "k3-256k",
  135. strings.HasPrefix(normalized, "kimi-"),
  136. strings.HasPrefix(normalized, "moonshot-"):
  137. return PlatformKimi, true
  138. case strings.HasPrefix(normalized, "glm-"):
  139. return PlatformZhipu, true
  140. case strings.HasPrefix(normalized, "deepseek-"):
  141. return PlatformDeepseek, true
  142. case strings.HasPrefix(normalized, "minimax-"),
  143. strings.HasPrefix(normalized, "abab5"),
  144. strings.HasPrefix(normalized, "abab6"),
  145. strings.HasPrefix(normalized, "abab7"):
  146. return PlatformMiniMax, true
  147. default:
  148. return "", false
  149. }
  150. }
  151. func hasOpenAISeriesPrefix(model string) bool {
  152. for _, prefix := range []string{"o1", "o3", "o4", "o5"} {
  153. if model == prefix || strings.HasPrefix(model, prefix+"-") {
  154. return true
  155. }
  156. }
  157. return false
  158. }
  159. func (s *GatewayService) resolveCompositeRouteDecision(ctx context.Context, group *Group, requestedModel, endpoint string) (CompositeRouteDecision, bool, error) {
  160. if group == nil || group.Platform != PlatformComposite {
  161. return CompositeRouteDecision{}, false, nil
  162. }
  163. if platform, ok := ResolvedTargetPlatformFromContext(ctx); ok {
  164. upstreamModel := requestedModel
  165. if resolvedModel, modelOK := ResolvedUpstreamModelFromContext(ctx); modelOK {
  166. upstreamModel = resolvedModel
  167. }
  168. source := CompositeRouteSourceDetector
  169. if resolvedSource, sourceOK := CompositeRouteSourceFromContext(ctx); sourceOK {
  170. source = resolvedSource
  171. }
  172. return CompositeRouteDecision{
  173. Matched: true,
  174. Source: source,
  175. GroupID: group.ID,
  176. PublicModel: requestedModel,
  177. TargetPlatform: platform,
  178. UpstreamModel: upstreamModel,
  179. Endpoint: normalizeCompositeRouteEndpoint(endpoint),
  180. }, true, nil
  181. }
  182. decision, err := s.compositeResolver.Resolve(ctx, group.ID, requestedModel, endpoint)
  183. if err != nil {
  184. return decision, false, err
  185. }
  186. return decision, decision.Matched, nil
  187. }
  188. func isConcreteRequestPlatform(platform string) bool {
  189. switch platform {
  190. case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
  191. PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax:
  192. return true
  193. default:
  194. return false
  195. }
  196. }