| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- 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
- case "minimax":
- return PlatformMiniMax, 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 normalized == "k3",
- normalized == "k3-256k",
- 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
- case strings.HasPrefix(normalized, "minimax-"),
- strings.HasPrefix(normalized, "abab5"),
- strings.HasPrefix(normalized, "abab6"),
- strings.HasPrefix(normalized, "abab7"):
- return PlatformMiniMax, 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, PlatformMiniMax:
- return true
- default:
- return false
- }
- }
|