composite_route_resolver.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. )
  8. type CompositeRouteResolver struct {
  9. repo CompositeModelRouteRepository
  10. modelOwnershipResolver CompositeModelOwnershipResolver
  11. }
  12. func NewCompositeRouteResolver(repo CompositeModelRouteRepository) *CompositeRouteResolver {
  13. return &CompositeRouteResolver{repo: repo}
  14. }
  15. func (r *CompositeRouteResolver) SetModelOwnershipResolver(resolver CompositeModelOwnershipResolver) {
  16. if r != nil {
  17. r.modelOwnershipResolver = resolver
  18. }
  19. }
  20. func (r *CompositeRouteResolver) Resolve(ctx context.Context, groupID int64, model, endpoint string) (CompositeRouteDecision, error) {
  21. model = strings.TrimSpace(model)
  22. endpoint = normalizeCompositeRouteEndpoint(endpoint)
  23. decision := CompositeRouteDecision{
  24. GroupID: groupID,
  25. PublicModel: model,
  26. Endpoint: endpoint,
  27. }
  28. if model == "" {
  29. decision.Reason = "model is required"
  30. return decision, nil
  31. }
  32. if r != nil && r.repo != nil && groupID > 0 {
  33. routes, err := r.repo.ListByGroup(ctx, groupID, false)
  34. if err != nil {
  35. return decision, fmt.Errorf("list composite routes: %w", err)
  36. }
  37. if route, ok := matchCompositeRoute(routes, model, endpoint); ok {
  38. upstreamModel := strings.TrimSpace(route.UpstreamModel)
  39. if upstreamModel == "" {
  40. upstreamModel = model
  41. }
  42. return CompositeRouteDecision{
  43. Matched: true,
  44. Source: CompositeRouteSourceExplicit,
  45. GroupID: groupID,
  46. PublicModel: model,
  47. TargetPlatform: route.TargetPlatform,
  48. UpstreamModel: upstreamModel,
  49. Endpoint: endpoint,
  50. Route: &route,
  51. }, nil
  52. }
  53. }
  54. if r != nil && r.modelOwnershipResolver != nil && groupID > 0 {
  55. ownership, err := r.modelOwnershipResolver(ctx, groupID, model)
  56. if err != nil {
  57. // A recognizable model can still use the existing detector when the
  58. // account catalog is temporarily unavailable. Unknown aliases cannot.
  59. if _, detectable := DetectModelPlatform(model); !detectable {
  60. return decision, fmt.Errorf("resolve account model ownership: %w", err)
  61. }
  62. } else if ownership.Ambiguous {
  63. decision.Reason = "model is exposed by multiple provider platforms"
  64. return decision, nil
  65. } else if ownership.Matched {
  66. platform := strings.TrimSpace(ownership.TargetPlatform)
  67. if !isConcreteRequestPlatform(platform) {
  68. decision.Reason = "account model ownership has no concrete target platform"
  69. return decision, nil
  70. }
  71. return CompositeRouteDecision{
  72. Matched: true,
  73. Source: CompositeRouteSourceAccount,
  74. GroupID: groupID,
  75. PublicModel: model,
  76. TargetPlatform: platform,
  77. UpstreamModel: model,
  78. Endpoint: endpoint,
  79. }, nil
  80. }
  81. }
  82. if platform, ok := DetectModelPlatform(model); ok {
  83. return CompositeRouteDecision{
  84. Matched: true,
  85. Source: CompositeRouteSourceDetector,
  86. GroupID: groupID,
  87. PublicModel: model,
  88. TargetPlatform: platform,
  89. UpstreamModel: model,
  90. Endpoint: endpoint,
  91. }, nil
  92. }
  93. decision.Reason = "no explicit route or built-in detector match"
  94. return decision, nil
  95. }
  96. func matchCompositeRoute(routes []CompositeModelRoute, model, endpoint string) (CompositeModelRoute, bool) {
  97. if len(routes) == 0 {
  98. return CompositeModelRoute{}, false
  99. }
  100. type candidate struct {
  101. route CompositeModelRoute
  102. matchStrength int
  103. endpointWeight int
  104. prefixLen int
  105. }
  106. candidates := make([]candidate, 0, len(routes))
  107. for _, route := range routes {
  108. route.Endpoint = normalizeCompositeRouteEndpoint(route.Endpoint)
  109. if route.Endpoint != endpoint && route.Endpoint != CompositeRouteEndpointAny {
  110. continue
  111. }
  112. route.MatchType = normalizeCompositeRouteMatchType(route.MatchType)
  113. publicModel := strings.TrimSpace(route.PublicModel)
  114. if publicModel == "" {
  115. continue
  116. }
  117. matchStrength := 0
  118. prefixLen := len(publicModel)
  119. switch route.MatchType {
  120. case CompositeRouteMatchExact:
  121. if publicModel != model {
  122. continue
  123. }
  124. matchStrength = 2
  125. case CompositeRouteMatchPrefix:
  126. if !strings.HasPrefix(model, publicModel) {
  127. continue
  128. }
  129. matchStrength = 1
  130. default:
  131. continue
  132. }
  133. endpointWeight := 0
  134. if route.Endpoint == endpoint {
  135. endpointWeight = 1
  136. }
  137. candidates = append(candidates, candidate{
  138. route: route,
  139. matchStrength: matchStrength,
  140. endpointWeight: endpointWeight,
  141. prefixLen: prefixLen,
  142. })
  143. }
  144. if len(candidates) == 0 {
  145. return CompositeModelRoute{}, false
  146. }
  147. sort.SliceStable(candidates, func(i, j int) bool {
  148. a, b := candidates[i], candidates[j]
  149. if a.matchStrength != b.matchStrength {
  150. return a.matchStrength > b.matchStrength
  151. }
  152. if a.endpointWeight != b.endpointWeight {
  153. return a.endpointWeight > b.endpointWeight
  154. }
  155. if a.prefixLen != b.prefixLen {
  156. return a.prefixLen > b.prefixLen
  157. }
  158. if a.route.Priority != b.route.Priority {
  159. return a.route.Priority < b.route.Priority
  160. }
  161. return a.route.ID < b.route.ID
  162. })
  163. return candidates[0].route, true
  164. }