Files
sub2api/backend/internal/service/composite_platform.go
T
李建琦 6d655c9903
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
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

202 lines
6.2 KiB
Go

package service
import (
"context"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
)
// WithResolvedTargetPlatform stores the concrete provider chosen for a request
// made through a composite group.
func WithResolvedTargetPlatform(ctx context.Context, platform string) context.Context {
platform = strings.TrimSpace(platform)
if ctx == nil || platform == "" {
return ctx
}
return context.WithValue(ctx, ctxkey.ResolvedTargetPlatform, platform)
}
// ResolvedTargetPlatformFromContext returns the concrete provider chosen for
// the current request, if one was resolved.
func ResolvedTargetPlatformFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
platform, ok := ctx.Value(ctxkey.ResolvedTargetPlatform).(string)
platform = strings.TrimSpace(platform)
if !ok || platform == "" {
return "", false
}
return platform, true
}
func WithCompositeRouteDecision(ctx context.Context, decision CompositeRouteDecision) context.Context {
if ctx == nil || !decision.Matched {
return ctx
}
ctx = WithResolvedTargetPlatform(ctx, decision.TargetPlatform)
if model := strings.TrimSpace(decision.UpstreamModel); model != "" {
ctx = context.WithValue(ctx, ctxkey.ResolvedUpstreamModel, model)
}
if model := strings.TrimSpace(decision.PublicModel); model != "" {
ctx = context.WithValue(ctx, ctxkey.RequestedPublicModel, model)
}
if source := strings.TrimSpace(decision.Source); source != "" {
ctx = context.WithValue(ctx, ctxkey.CompositeRouteSource, source)
}
return ctx
}
func ResolvedUpstreamModelFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
model, ok := ctx.Value(ctxkey.ResolvedUpstreamModel).(string)
model = strings.TrimSpace(model)
if !ok || model == "" {
return "", false
}
return model, true
}
func RequestedPublicModelFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
model, ok := ctx.Value(ctxkey.RequestedPublicModel).(string)
model = strings.TrimSpace(model)
if !ok || model == "" {
return "", false
}
return model, true
}
func CompositeRouteSourceFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
source, ok := ctx.Value(ctxkey.CompositeRouteSource).(string)
source = strings.TrimSpace(source)
if !ok || source == "" {
return "", false
}
return source, true
}
// DetectModelPlatform maps common public model IDs to the concrete provider
// platform used by sub2api. It intentionally returns false for ambiguous model
// names so composite groups fail closed instead of guessing.
func DetectModelPlatform(model string) (string, bool) {
normalized := strings.ToLower(strings.TrimSpace(model))
if normalized == "" {
return "", false
}
normalized = strings.TrimPrefix(normalized, "models/")
if slash := strings.IndexByte(normalized, '/'); slash > 0 {
provider := strings.TrimSpace(normalized[:slash])
rest := strings.TrimSpace(normalized[slash+1:])
switch provider {
case "anthropic", "claude":
return PlatformAnthropic, true
case "openai", "chatgpt":
return PlatformOpenAI, true
case "google", "google-ai-studio", "gemini":
return PlatformGemini, true
case "xai", "x-ai", "grok":
return PlatformGrok, true
case "kimi", "moonshot":
return PlatformKimi, true
case "zhipu", "glm", "bigmodel":
return PlatformZhipu, true
case "deepseek":
return PlatformDeepseek, true
}
if rest != "" {
normalized = strings.TrimPrefix(rest, "models/")
}
}
switch {
case strings.HasPrefix(normalized, "anthropic.claude-"),
strings.HasPrefix(normalized, "claude-"):
return PlatformAnthropic, true
case strings.HasPrefix(normalized, "gpt-"),
strings.HasPrefix(normalized, "chatgpt-"),
strings.HasPrefix(normalized, "codex-"),
strings.HasPrefix(normalized, "text-embedding-"),
strings.HasPrefix(normalized, "text-moderation-"),
strings.HasPrefix(normalized, "omni-moderation-"),
strings.HasPrefix(normalized, "dall-e-"),
strings.HasPrefix(normalized, "gpt-image-"),
strings.HasPrefix(normalized, "tts-"),
strings.HasPrefix(normalized, "whisper-"),
hasOpenAISeriesPrefix(normalized):
return PlatformOpenAI, true
case strings.HasPrefix(normalized, "gemini-"),
strings.HasPrefix(normalized, "learnlm-"):
return PlatformGemini, true
case normalized == "grok" || strings.HasPrefix(normalized, "grok-"):
return PlatformGrok, true
case strings.HasPrefix(normalized, "kimi-"),
strings.HasPrefix(normalized, "moonshot-"):
return PlatformKimi, true
case strings.HasPrefix(normalized, "glm-"):
return PlatformZhipu, true
case strings.HasPrefix(normalized, "deepseek-"):
return PlatformDeepseek, true
default:
return "", false
}
}
func hasOpenAISeriesPrefix(model string) bool {
for _, prefix := range []string{"o1", "o3", "o4", "o5"} {
if model == prefix || strings.HasPrefix(model, prefix+"-") {
return true
}
}
return false
}
func (s *GatewayService) resolveCompositeRouteDecision(ctx context.Context, group *Group, requestedModel, endpoint string) (CompositeRouteDecision, bool, error) {
if group == nil || group.Platform != PlatformComposite {
return CompositeRouteDecision{}, false, nil
}
if platform, ok := ResolvedTargetPlatformFromContext(ctx); ok {
upstreamModel := requestedModel
if resolvedModel, modelOK := ResolvedUpstreamModelFromContext(ctx); modelOK {
upstreamModel = resolvedModel
}
source := CompositeRouteSourceDetector
if resolvedSource, sourceOK := CompositeRouteSourceFromContext(ctx); sourceOK {
source = resolvedSource
}
return CompositeRouteDecision{
Matched: true,
Source: source,
GroupID: group.ID,
PublicModel: requestedModel,
TargetPlatform: platform,
UpstreamModel: upstreamModel,
Endpoint: normalizeCompositeRouteEndpoint(endpoint),
}, true, nil
}
decision, err := s.compositeResolver.Resolve(ctx, group.ID, requestedModel, endpoint)
if err != nil {
return decision, false, err
}
return decision, decision.Matched, nil
}
func isConcreteRequestPlatform(platform string) bool {
switch platform {
case PlatformAnthropic, PlatformOpenAI, PlatformGemini, PlatformAntigravity, PlatformGrok,
PlatformKimi, PlatformZhipu, PlatformDeepseek:
return true
default:
return false
}
}