composite_model_route.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "time"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. )
  8. const (
  9. CompositeRouteMatchExact = "exact"
  10. CompositeRouteMatchPrefix = "prefix"
  11. CompositeRouteEndpointAny = "any"
  12. CompositeRouteEndpointMessages = "messages"
  13. CompositeRouteEndpointCountTokens = "count_tokens"
  14. CompositeRouteEndpointResponses = "responses"
  15. CompositeRouteEndpointChatCompletions = "chat_completions"
  16. CompositeRouteEndpointEmbeddings = "embeddings"
  17. CompositeRouteEndpointImages = "images"
  18. CompositeRouteEndpointGemini = "gemini"
  19. CompositeRouteSourceExplicit = "route"
  20. CompositeRouteSourceDetector = "detector"
  21. CompositeRouteSourceAccount = "account_model"
  22. )
  23. // CompositeModelOwnership identifies the concrete provider that exposes a
  24. // public model through an account-level exact mapping.
  25. type CompositeModelOwnership struct {
  26. TargetPlatform string
  27. Matched bool
  28. Ambiguous bool
  29. }
  30. type CompositeModelOwnershipResolver func(context.Context, int64, string) (CompositeModelOwnership, error)
  31. var (
  32. ErrCompositeRouteNotFound = infraerrors.NotFound("COMPOSITE_ROUTE_NOT_FOUND", "composite route not found")
  33. ErrCompositeRouteExists = infraerrors.Conflict("COMPOSITE_ROUTE_EXISTS", "composite route already exists")
  34. )
  35. // CompositeModelRoute maps one public model identifier in a composite group to
  36. // the concrete provider/model that should handle the request.
  37. type CompositeModelRoute struct {
  38. ID int64 `json:"id"`
  39. GroupID int64 `json:"group_id"`
  40. PublicModel string `json:"public_model"`
  41. MatchType string `json:"match_type"`
  42. TargetPlatform string `json:"target_platform"`
  43. UpstreamModel string `json:"upstream_model"`
  44. Endpoint string `json:"endpoint"`
  45. Priority int `json:"priority"`
  46. Enabled bool `json:"enabled"`
  47. Notes string `json:"notes"`
  48. CreatedAt time.Time `json:"created_at"`
  49. UpdatedAt time.Time `json:"updated_at"`
  50. }
  51. type CompositeRoutePreviewRequest struct {
  52. Model string `json:"model"`
  53. Endpoint string `json:"endpoint"`
  54. }
  55. type CompositeRouteDecision struct {
  56. Matched bool `json:"matched"`
  57. Source string `json:"source"`
  58. GroupID int64 `json:"group_id"`
  59. PublicModel string `json:"public_model"`
  60. TargetPlatform string `json:"target_platform"`
  61. UpstreamModel string `json:"upstream_model"`
  62. Endpoint string `json:"endpoint"`
  63. Route *CompositeModelRoute `json:"route,omitempty"`
  64. Reason string `json:"reason,omitempty"`
  65. }
  66. type CompositeRouteInput struct {
  67. PublicModel string
  68. MatchType string
  69. TargetPlatform string
  70. UpstreamModel string
  71. Endpoint string
  72. Priority int
  73. Enabled bool
  74. Notes string
  75. }
  76. type CompositeModelRouteRepository interface {
  77. ListByGroup(ctx context.Context, groupID int64, includeDisabled bool) ([]CompositeModelRoute, error)
  78. Create(ctx context.Context, route *CompositeModelRoute) error
  79. Update(ctx context.Context, route *CompositeModelRoute) error
  80. Delete(ctx context.Context, id int64) error
  81. DeleteByGroup(ctx context.Context, groupID int64) error
  82. }
  83. func normalizeCompositeRouteEndpoint(endpoint string) string {
  84. endpoint = strings.ToLower(strings.TrimSpace(endpoint))
  85. if endpoint == "" {
  86. return CompositeRouteEndpointAny
  87. }
  88. switch endpoint {
  89. case CompositeRouteEndpointMessages,
  90. CompositeRouteEndpointCountTokens,
  91. CompositeRouteEndpointResponses,
  92. CompositeRouteEndpointChatCompletions,
  93. CompositeRouteEndpointEmbeddings,
  94. CompositeRouteEndpointImages,
  95. CompositeRouteEndpointGemini:
  96. return endpoint
  97. default:
  98. return CompositeRouteEndpointAny
  99. }
  100. }
  101. func normalizeCompositeRouteMatchType(matchType string) string {
  102. matchType = strings.ToLower(strings.TrimSpace(matchType))
  103. switch matchType {
  104. case CompositeRouteMatchPrefix:
  105. return CompositeRouteMatchPrefix
  106. default:
  107. return CompositeRouteMatchExact
  108. }
  109. }
  110. func normalizeCompositeRouteInput(input CompositeRouteInput) CompositeRouteInput {
  111. input.PublicModel = strings.TrimSpace(input.PublicModel)
  112. input.MatchType = normalizeCompositeRouteMatchType(input.MatchType)
  113. input.TargetPlatform = strings.TrimSpace(input.TargetPlatform)
  114. input.UpstreamModel = strings.TrimSpace(input.UpstreamModel)
  115. input.Endpoint = normalizeCompositeRouteEndpoint(input.Endpoint)
  116. // 仅对 exact 路由把空 upstream_model 回填成 public_model:exact 命中时请求模型
  117. // 恒等于 public_model,回填只影响持久化/后台展示,保留原有契约不变。
  118. // prefix 路由留空则不回填——Resolve 会回退到具体请求模型,从而透传原始模型
  119. // (否则 public=deepseek-v4 的前缀路由会把 deepseek-v4-flash / deepseek-v4-pro
  120. // 都塌缩成固定的 deepseek-v4)。显式填写 upstream_model 时任何模式都原样固定转发。
  121. if input.UpstreamModel == "" && input.MatchType == CompositeRouteMatchExact {
  122. input.UpstreamModel = input.PublicModel
  123. }
  124. input.Notes = strings.TrimSpace(input.Notes)
  125. return input
  126. }