132 lines
4.7 KiB
Go
132 lines
4.7 KiB
Go
package service
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
CompositeRouteMatchExact = "exact"
|
|||
|
|
CompositeRouteMatchPrefix = "prefix"
|
|||
|
|
|
|||
|
|
CompositeRouteEndpointAny = "any"
|
|||
|
|
CompositeRouteEndpointMessages = "messages"
|
|||
|
|
CompositeRouteEndpointCountTokens = "count_tokens"
|
|||
|
|
CompositeRouteEndpointResponses = "responses"
|
|||
|
|
CompositeRouteEndpointChatCompletions = "chat_completions"
|
|||
|
|
CompositeRouteEndpointEmbeddings = "embeddings"
|
|||
|
|
CompositeRouteEndpointImages = "images"
|
|||
|
|
CompositeRouteEndpointGemini = "gemini"
|
|||
|
|
|
|||
|
|
CompositeRouteSourceExplicit = "route"
|
|||
|
|
CompositeRouteSourceDetector = "detector"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
ErrCompositeRouteNotFound = infraerrors.NotFound("COMPOSITE_ROUTE_NOT_FOUND", "composite route not found")
|
|||
|
|
ErrCompositeRouteExists = infraerrors.Conflict("COMPOSITE_ROUTE_EXISTS", "composite route already exists")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// CompositeModelRoute maps one public model identifier in a composite group to
|
|||
|
|
// the concrete provider/model that should handle the request.
|
|||
|
|
type CompositeModelRoute struct {
|
|||
|
|
ID int64 `json:"id"`
|
|||
|
|
GroupID int64 `json:"group_id"`
|
|||
|
|
PublicModel string `json:"public_model"`
|
|||
|
|
MatchType string `json:"match_type"`
|
|||
|
|
TargetPlatform string `json:"target_platform"`
|
|||
|
|
UpstreamModel string `json:"upstream_model"`
|
|||
|
|
Endpoint string `json:"endpoint"`
|
|||
|
|
Priority int `json:"priority"`
|
|||
|
|
Enabled bool `json:"enabled"`
|
|||
|
|
Notes string `json:"notes"`
|
|||
|
|
CreatedAt time.Time `json:"created_at"`
|
|||
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CompositeRoutePreviewRequest struct {
|
|||
|
|
Model string `json:"model"`
|
|||
|
|
Endpoint string `json:"endpoint"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CompositeRouteDecision struct {
|
|||
|
|
Matched bool `json:"matched"`
|
|||
|
|
Source string `json:"source"`
|
|||
|
|
GroupID int64 `json:"group_id"`
|
|||
|
|
PublicModel string `json:"public_model"`
|
|||
|
|
TargetPlatform string `json:"target_platform"`
|
|||
|
|
UpstreamModel string `json:"upstream_model"`
|
|||
|
|
Endpoint string `json:"endpoint"`
|
|||
|
|
Route *CompositeModelRoute `json:"route,omitempty"`
|
|||
|
|
Reason string `json:"reason,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CompositeRouteInput struct {
|
|||
|
|
PublicModel string
|
|||
|
|
MatchType string
|
|||
|
|
TargetPlatform string
|
|||
|
|
UpstreamModel string
|
|||
|
|
Endpoint string
|
|||
|
|
Priority int
|
|||
|
|
Enabled bool
|
|||
|
|
Notes string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type CompositeModelRouteRepository interface {
|
|||
|
|
ListByGroup(ctx context.Context, groupID int64, includeDisabled bool) ([]CompositeModelRoute, error)
|
|||
|
|
Create(ctx context.Context, route *CompositeModelRoute) error
|
|||
|
|
Update(ctx context.Context, route *CompositeModelRoute) error
|
|||
|
|
Delete(ctx context.Context, id int64) error
|
|||
|
|
DeleteByGroup(ctx context.Context, groupID int64) error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeCompositeRouteEndpoint(endpoint string) string {
|
|||
|
|
endpoint = strings.ToLower(strings.TrimSpace(endpoint))
|
|||
|
|
if endpoint == "" {
|
|||
|
|
return CompositeRouteEndpointAny
|
|||
|
|
}
|
|||
|
|
switch endpoint {
|
|||
|
|
case CompositeRouteEndpointMessages,
|
|||
|
|
CompositeRouteEndpointCountTokens,
|
|||
|
|
CompositeRouteEndpointResponses,
|
|||
|
|
CompositeRouteEndpointChatCompletions,
|
|||
|
|
CompositeRouteEndpointEmbeddings,
|
|||
|
|
CompositeRouteEndpointImages,
|
|||
|
|
CompositeRouteEndpointGemini:
|
|||
|
|
return endpoint
|
|||
|
|
default:
|
|||
|
|
return CompositeRouteEndpointAny
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeCompositeRouteMatchType(matchType string) string {
|
|||
|
|
matchType = strings.ToLower(strings.TrimSpace(matchType))
|
|||
|
|
switch matchType {
|
|||
|
|
case CompositeRouteMatchPrefix:
|
|||
|
|
return CompositeRouteMatchPrefix
|
|||
|
|
default:
|
|||
|
|
return CompositeRouteMatchExact
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func normalizeCompositeRouteInput(input CompositeRouteInput) CompositeRouteInput {
|
|||
|
|
input.PublicModel = strings.TrimSpace(input.PublicModel)
|
|||
|
|
input.MatchType = normalizeCompositeRouteMatchType(input.MatchType)
|
|||
|
|
input.TargetPlatform = strings.TrimSpace(input.TargetPlatform)
|
|||
|
|
input.UpstreamModel = strings.TrimSpace(input.UpstreamModel)
|
|||
|
|
input.Endpoint = normalizeCompositeRouteEndpoint(input.Endpoint)
|
|||
|
|
// 仅对 exact 路由把空 upstream_model 回填成 public_model:exact 命中时请求模型
|
|||
|
|
// 恒等于 public_model,回填只影响持久化/后台展示,保留原有契约不变。
|
|||
|
|
// prefix 路由留空则不回填——Resolve 会回退到具体请求模型,从而透传原始模型
|
|||
|
|
// (否则 public=deepseek-v4 的前缀路由会把 deepseek-v4-flash / deepseek-v4-pro
|
|||
|
|
// 都塌缩成固定的 deepseek-v4)。显式填写 upstream_model 时任何模式都原样固定转发。
|
|||
|
|
if input.UpstreamModel == "" && input.MatchType == CompositeRouteMatchExact {
|
|||
|
|
input.UpstreamModel = input.PublicModel
|
|||
|
|
}
|
|||
|
|
input.Notes = strings.TrimSpace(input.Notes)
|
|||
|
|
return input
|
|||
|
|
}
|