Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
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
|
||
}
|